Skip to content

Commit 6c54b20

Browse files
committed
Fix issue Mass Export is giving Invalid Column Name: COLUMN_POSITION for each table exported
Fixes #130
1 parent 5dd1a06 commit 6c54b20

File tree

8 files changed

+359
-7
lines changed

8 files changed

+359
-7
lines changed

bin/import.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,14 @@ export async function importData(prompts) {
10801080
throw new Error(baseLite.bundle.getText("errInvalidTableFormat", [prompts.table]))
10811081
}
10821082

1083+
if (!schema && prompts.schema && prompts.schema !== '**CURRENT_SCHEMA**') {
1084+
const parsedSchema = parseIdentifier(prompts.schema, dbKind)
1085+
if (!parsedSchema.name) {
1086+
throw new Error(baseLite.bundle.getText("errInvalidSchema", [prompts.schema]))
1087+
}
1088+
schema = parsedSchema.name
1089+
}
1090+
10831091
if (!schema && dbKind !== 'sqlite') {
10841092
const currentSchema = await getCurrentSchema(dbClient, dbKind)
10851093
if (!currentSchema) {

bin/massExport.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ export const builder = (yargs) => yargs.options(baseLite.getBuilder({
1010
schema: {
1111
alias: ['s', 'schema'],
1212
type: 'string',
13+
default: '**CURRENT_SCHEMA**',
1314
desc: baseLite.bundle.getText("schema")
1415
},
1516
object: {
1617
alias: ['o', 'object'],
1718
type: 'string',
19+
default: '*',
1820
desc: baseLite.bundle.getText("object")
1921
},
2022
objectType: {
@@ -35,7 +37,7 @@ export const builder = (yargs) => yargs.options(baseLite.getBuilder({
3537
desc: baseLite.bundle.getText("exportFormat")
3638
},
3739
folder: {
38-
alias: ['d', 'directory'],
40+
alias: ['directory', 'dir'],
3941
type: 'string',
4042
desc: baseLite.bundle.getText("folder")
4143
},
@@ -53,12 +55,14 @@ export async function handler (argv) {
5355
schema: {
5456
type: 'string',
5557
description: base.bundle.getText("schema"),
56-
required: true
58+
required: false,
59+
default: '**CURRENT_SCHEMA**'
5760
},
5861
object: {
5962
type: 'string',
6063
description: base.bundle.getText("object"),
61-
required: true
64+
required: false,
65+
default: '*'
6266
},
6367
objectType: {
6468
type: 'string',
@@ -80,7 +84,7 @@ export async function handler (argv) {
8084
folder: {
8185
type: 'string',
8286
description: base.bundle.getText("folder"),
83-
required: true
87+
required: false
8488
},
8589
includeData: {
8690
type: 'boolean',

tests/aliasConflicts.Test.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// @ts-check
2+
3+
import yargs from 'yargs/yargs'
4+
import path from 'path'
5+
import { pathToFileURL } from 'url'
6+
import * as assert from 'assert'
7+
import { commandMap } from '../bin/commandMap.js'
8+
9+
/**
10+
* Build unique command module file list from command map values.
11+
* @returns {string[]}
12+
*/
13+
function getUniqueCommandFiles() {
14+
return [...new Set(Object.values(commandMap))]
15+
}
16+
17+
/**
18+
* Extract command name token from yargs command export.
19+
* @param {string} commandSignature
20+
* @returns {string}
21+
*/
22+
function getCommandToken(commandSignature) {
23+
return String(commandSignature || '').trim().split(/\s+/)[0]
24+
}
25+
26+
describe('@all alias conflict guardrails', function () {
27+
this.timeout(20000)
28+
29+
const knownCommandAliasConflicts = new Set([
30+
'changes -> ./openChangeLog.js and ./changeLog.js',
31+
'changeLog -> ./openChangeLog.js and ./changeLog.js',
32+
'changelog -> ./openChangeLog.js and ./changeLog.js',
33+
'c -> ./connect.js and ./connections.js',
34+
'dataCompare -> ./compareData.js and ./dataDiff.js',
35+
'docs -> ./generateDocs.js and ./viewDocs.js',
36+
'documentation -> ./helpDocu.js and ./viewDocs.js',
37+
'iniFiles -> ./iniContents.js and ./iniFiles.js',
38+
'if -> ./iniContents.js and ./iniFiles.js',
39+
'inifiles -> ./iniContents.js and ./iniFiles.js',
40+
'ini -> ./iniContents.js and ./iniFiles.js',
41+
'if -> ./iniContents.js and ./inspectFunction.js',
42+
'mu -> ./massUpdate.js and ./massUsers.js',
43+
'readme -> ./readMe.js and ./openReadMe.js',
44+
'listschemas -> ./schemas.js and ./hanaCloudSchemaInstances.js',
45+
'listschemasui -> ./schemasUI.js and ./hanaCloudSchemaInstancesUI.js',
46+
's -> ./schemas.js and ./status.js'
47+
])
48+
49+
const knownOptionAliasConflicts = new Set([
50+
'./auditLog.js: -a used by both admin and action',
51+
'./auditLog.js: -d used by both debug and days',
52+
'./adminHDI.js: -p used by both profile and password',
53+
'./blocking.js: -d used by both debug and details',
54+
'./callProcedure.js: -p used by both procedure and profile',
55+
'./connections.js: -a used by both admin and application',
56+
'./createXSAAdmin.js: -p used by both profile and password',
57+
'./crashDumps.js: -d used by both debug and days',
58+
'./encryptionStatus.js: -d used by both debug and details',
59+
'./export.js: -d used by both debug and delimiter',
60+
'./ftIndexes.js: -d used by both debug and details',
61+
'./grantChains.js: -d used by both debug and depth',
62+
'./inspectProcedure.js: -p used by both profile and procedure',
63+
'./longRunning.js: -d used by both debug and duration',
64+
'./massRename.js: -p used by both profile and prefix',
65+
'./massUsers.js: -p used by both profile and password',
66+
'./procedures.js: -p used by both procedure and profile',
67+
'./pwdPolicy.js: -p used by both profile and policy',
68+
'./pwdPolicy.js: -d used by both debug and details',
69+
'./replicationStatus.js: -d used by both debug and detailed',
70+
'./securityScan.js: -d used by both debug and detailed',
71+
'./sdiTasks.js: -a used by both admin and action',
72+
'./status.js: -p used by both profile and priv',
73+
'./tableHotspots.js: -p used by both includePartitions and profile',
74+
'./tableGroups.js: -a used by both admin and action',
75+
'./xsaServices.js: -a used by both admin and action',
76+
'./xsaServices.js: -d used by both debug and details',
77+
'./workloadManagement.js: -p used by both profile and priority',
78+
'./workloadManagement.js: -a used by both admin and activeOnly',
79+
'./kafkaConnect.js: -a used by both admin and action',
80+
'./timeSeriesTools.js: -a used by both admin and action'
81+
])
82+
83+
it('does not introduce new duplicate command aliases across command modules', async function () {
84+
const files = getUniqueCommandFiles()
85+
/** @type {Map<string, string>} */
86+
const ownerByAlias = new Map()
87+
/** @type {Array<string>} */
88+
const conflicts = []
89+
90+
for (const relFile of files) {
91+
const absFile = path.resolve('bin', relFile.replace(/^\.\//, ''))
92+
const mod = await import(pathToFileURL(absFile).href)
93+
94+
const commandToken = getCommandToken(mod.command)
95+
const aliases = Array.isArray(mod.aliases) ? mod.aliases : []
96+
const allNames = [commandToken, ...aliases].filter(Boolean)
97+
98+
for (const name of allNames) {
99+
const key = String(name).toLowerCase()
100+
const owner = ownerByAlias.get(key)
101+
if (owner && owner !== relFile) {
102+
conflicts.push(`${name} -> ${owner} and ${relFile}`)
103+
} else if (!owner) {
104+
ownerByAlias.set(key, relFile)
105+
}
106+
}
107+
}
108+
109+
const unexpected = conflicts.filter((c) => !knownCommandAliasConflicts.has(c))
110+
111+
assert.strictEqual(
112+
unexpected.length,
113+
0,
114+
`New duplicate command aliases introduced:\n${unexpected.join('\n')}`
115+
)
116+
})
117+
118+
it('does not introduce new option alias collisions within command builders', async function () {
119+
const files = getUniqueCommandFiles()
120+
/** @type {Array<string>} */
121+
const conflicts = []
122+
123+
for (const relFile of files) {
124+
const absFile = path.resolve('bin', relFile.replace(/^\.\//, ''))
125+
const mod = await import(pathToFileURL(absFile).href)
126+
127+
if (typeof mod.builder !== 'function') {
128+
continue
129+
}
130+
131+
const parser = mod.builder(
132+
yargs([])
133+
.help(false)
134+
.version(false)
135+
.exitProcess(false)
136+
)
137+
138+
const aliasMap = parser.getOptions().alias || {}
139+
/** @type {Map<string, string>} */
140+
const aliasOwner = new Map()
141+
142+
for (const [optionName, aliasValues] of Object.entries(aliasMap)) {
143+
const aliases = Array.isArray(aliasValues) ? aliasValues : [aliasValues]
144+
145+
for (const alias of aliases) {
146+
const aliasText = String(alias)
147+
if (!aliasText || aliasText === optionName) {
148+
continue
149+
}
150+
151+
const owner = aliasOwner.get(aliasText)
152+
if (owner && owner !== optionName) {
153+
conflicts.push(`${relFile}: -${aliasText} used by both ${owner} and ${optionName}`)
154+
} else if (!owner) {
155+
aliasOwner.set(aliasText, String(optionName))
156+
}
157+
}
158+
}
159+
}
160+
161+
const unexpected = conflicts.filter((c) => !knownOptionAliasConflicts.has(c))
162+
163+
assert.strictEqual(
164+
unexpected.length,
165+
0,
166+
`New option alias collisions introduced:\n${unexpected.join('\n')}`
167+
)
168+
})
169+
})

tests/export.Test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import * as chai from 'chai'
33
import * as chaiAsPromised from 'chai-as-promised'
44
import sinon from 'sinon'
55
import * as fs from 'fs'
6+
import yargsModule from 'yargs'
67

78
chai.use(chaiAsPromised.default)
89
const expect = chai.expect
10+
const yargs = yargsModule
911

1012
/**
1113
* @test Export Command Tests
@@ -87,6 +89,26 @@ describe('@all @export', () => {
8789
expect(exportCmd.inputPrompts.limit.type).to.equal('number')
8890
expect(exportCmd.inputPrompts.limit.required).to.equal(false)
8991
})
92+
93+
it('should default schema to CURRENT_SCHEMA when omitted', async () => {
94+
const parser = exportCmd.builder(yargs([])
95+
.help(false)
96+
.version(false)
97+
.exitProcess(false))
98+
99+
const argv = await parser.parse(['--table', 'DUMMY', '--output', 'dummy.json', '--format', 'json'])
100+
expect(argv.schema).to.equal('**CURRENT_SCHEMA**')
101+
})
102+
103+
it('should parse explicit schema value', async () => {
104+
const parser = exportCmd.builder(yargs([])
105+
.help(false)
106+
.version(false)
107+
.exitProcess(false))
108+
109+
const argv = await parser.parse(['--table', 'DUMMY', '--output', 'dummy.json', '--format', 'json', '--schema', 'MYSCHEMA'])
110+
expect(argv.schema).to.equal('MYSCHEMA')
111+
})
90112
})
91113

92114
describe('inputPrompts configuration', () => {

tests/import.Test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,26 @@ Jane,25,jane@example.com`
110110
it('should provide truncate boolean option', () => {
111111
// Truncate should be a boolean flag
112112
})
113+
114+
it('should default schema to CURRENT_SCHEMA when omitted', async () => {
115+
const parser = importCmd.builder(yargs([])
116+
.help(false)
117+
.version(false)
118+
.exitProcess(false))
119+
120+
const argv = await parser.parse(['--filename', 'data.csv', '--table', 'DUMMY'])
121+
expect(argv.schema).to.equal('**CURRENT_SCHEMA**')
122+
})
123+
124+
it('should parse explicit schema value', async () => {
125+
const parser = importCmd.builder(yargs([])
126+
.help(false)
127+
.version(false)
128+
.exitProcess(false))
129+
130+
const argv = await parser.parse(['--filename', 'data.csv', '--table', 'DUMMY', '--schema', 'MYSCHEMA'])
131+
expect(argv.schema).to.equal('MYSCHEMA')
132+
})
113133
})
114134

115135
describe('import functionality (mock)', () => {

tests/massExport.Test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// @ts-check
22
import * as base from './base.js'
3+
import yargs from 'yargs/yargs'
4+
import { assert } from './base.js'
35

46
describe('massExport', function () {
57

@@ -8,4 +10,47 @@ describe('massExport', function () {
810
localTest("node bin/massExport.js --help", done)
911
})
1012

13+
it('parses --folder as folder and not schema', async function () {
14+
const massExportCmd = await import('../bin/massExport.js')
15+
16+
const parser = massExportCmd.builder(yargs([])
17+
.scriptName('hana-cli')
18+
.help(false)
19+
.version(false)
20+
.exitProcess(false))
21+
22+
const argv = await parser.parse(['--object', '%', '-t', 'TABLE', '--data', '--folder', './tmp'])
23+
24+
assert.strictEqual(argv.folder, './tmp')
25+
assert.notStrictEqual(argv.schema, './tmp')
26+
})
27+
28+
it('defaults schema to CURRENT_SCHEMA when schema is omitted', async function () {
29+
const massExportCmd = await import('../bin/massExport.js')
30+
31+
const parser = massExportCmd.builder(yargs([])
32+
.scriptName('hana-cli')
33+
.help(false)
34+
.version(false)
35+
.exitProcess(false))
36+
37+
const argv = await parser.parse(['--object', '%', '--folder', './tmp'])
38+
39+
assert.strictEqual(argv.schema, '**CURRENT_SCHEMA**')
40+
})
41+
42+
it('defaults object to wildcard when object is omitted', async function () {
43+
const massExportCmd = await import('../bin/massExport.js')
44+
45+
const parser = massExportCmd.builder(yargs([])
46+
.scriptName('hana-cli')
47+
.help(false)
48+
.version(false)
49+
.exitProcess(false))
50+
51+
const argv = await parser.parse(['--folder', './tmp'])
52+
53+
assert.strictEqual(argv.object, '*')
54+
})
55+
1156
})

0 commit comments

Comments
 (0)