Skip to content

Commit d469001

Browse files
committed
Add unique suffixes for duplicate names in OUT_FORMAT_OBJECT mode
1 parent a0a57ff commit d469001

File tree

5 files changed

+279
-0
lines changed

5 files changed

+279
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
objects" or kept as database-backed objects. This option also applies to
1414
output `BIND_OUT` bind variables.
1515

16+
- Numeric suffixes are now added to duplicate SELECT column names when using
17+
`oracledb.OUT_FORMAT_OBJECT` mode, allowing all columns to be represented in
18+
the JavaScript object.
19+
1620
- Fixed a compatibility regression affecting SODA "get" operations using older
1721
Oracle Client releases.
1822

doc/api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,6 +1593,10 @@ property value set to the respective column value. The property name follows
15931593
Oracle's standard name-casing rules. It will commonly be uppercase, since most
15941594
applications create tables using unquoted, case-insensitive names.
15951595

1596+
From node-oracledb 5.1, when duplicate column names are used in queries, then
1597+
node-oracledb will append numeric suffixes in `oracledb.OUT_FORMAT_OBJECT` mode
1598+
as necessary, so that all columns are represented in the JavaScript object.
1599+
15961600
This property may be overridden in
15971601
an [`execute()`](#executeoptions)
15981602
or [`queryStream()`](#querystream) call.

src/njsConnection.c

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ static napi_value njsConnection_setTextAttribute(napi_env env,
217217
int (*setter)(dpiConn*, const char *, uint32_t));
218218
static bool njsConnection_transferExecuteManyBinds(njsBaton *baton,
219219
napi_env env, napi_value binds, napi_value bindNames);
220+
static bool njsConnection_makeUniqueColumnNames(napi_env env, njsBaton *baton,
221+
njsVariable *queryVars, uint32_t numQueryVars);
220222

221223

222224
//-----------------------------------------------------------------------------
@@ -619,6 +621,14 @@ static bool njsConnection_executePostAsync(njsBaton *baton, napi_env env,
619621
env, baton))
620622
return false;
621623

624+
// check for duplicate column names when using OBJECT output and append
625+
// "_xx" for any duplicate names found
626+
if (baton->outFormat == NJS_ROWS_OBJECT) {
627+
if (!njsConnection_makeUniqueColumnNames(env, baton,
628+
baton->queryVars, baton->numQueryVars))
629+
return njsBaton_setError (baton, errInsufficientMemory);
630+
}
631+
622632
// set metadata for the query
623633
if (!njsVariable_getMetadataMany(baton->queryVars, baton->numQueryVars,
624634
env, baton->extendedMetaData, &metadata))
@@ -3146,3 +3156,76 @@ static bool njsConnection_unsubscribeAsync(njsBaton *baton)
31463156
baton->subscription = NULL;
31473157
return true;
31483158
}
3159+
3160+
3161+
//----------------------------------------------------------------------------
3162+
// njsConnection_makeUniqueColumnNames()
3163+
// Check for duplicate column names, and append "_xx" to make names unique
3164+
//
3165+
// Parameters
3166+
// env - napi env variable
3167+
// baton - baton structure
3168+
// queryVars - njsVariables struct for Query SQLs
3169+
// numQueryVars - number of Query Variables
3170+
//---------------------------------------------------------------------------
3171+
static bool njsConnection_makeUniqueColumnNames(napi_env env, njsBaton *baton,
3172+
njsVariable *queryVars, uint32_t numQueryVars)
3173+
{
3174+
char tempName[NJS_MAX_COL_NAME_BUFFER_LENGTH];
3175+
uint32_t tempNum, col, index;
3176+
napi_value tempObj, colObj;
3177+
size_t tempNameLength;
3178+
bool exists;
3179+
3180+
// First loop creates a napi-object(hash table) with unique column name &
3181+
// column number first appeared for later use.
3182+
NJS_CHECK_NAPI(env, napi_create_object(env, &tempObj))
3183+
for (col = 0; col < numQueryVars; col ++) {
3184+
NJS_CHECK_NAPI(env, napi_create_string_utf8(env, queryVars[col].name,
3185+
queryVars[col].nameLength, &queryVars[col].jsName))
3186+
3187+
NJS_CHECK_NAPI(env, napi_has_property(env, tempObj,
3188+
queryVars[col].jsName, &exists))
3189+
if (!exists) {
3190+
NJS_CHECK_NAPI(env, napi_create_uint32(env, col, &colObj))
3191+
NJS_CHECK_NAPI(env, napi_set_property(env, tempObj,
3192+
queryVars[col].jsName, colObj))
3193+
}
3194+
}
3195+
3196+
// Second loop looks for the current column name in the napi-object,
3197+
// if exists and column number is different, (then it is duplicate),
3198+
// tries to compose a name to resolve the duplicate name.
3199+
// The composed name is also checked in the napi-object before updating
3200+
// to make sure there are no conflicts.
3201+
for (col = 0; col < numQueryVars; col ++) {
3202+
NJS_CHECK_NAPI(env, napi_get_property(env, tempObj,
3203+
queryVars[col].jsName, &colObj))
3204+
NJS_CHECK_NAPI(env, napi_get_value_uint32(env, colObj, &index))
3205+
3206+
if (index != col) {
3207+
exists = true;
3208+
tempNum = 0;
3209+
while (exists) {
3210+
tempNameLength = (size_t) snprintf(tempName, sizeof (tempName),
3211+
"%.*s_%d", (int)queryVars[col].nameLength,
3212+
queryVars[col].name, ++tempNum);
3213+
if (tempNameLength > (sizeof(tempName) - 1))
3214+
tempNameLength = sizeof(tempName) - 1;
3215+
3216+
NJS_CHECK_NAPI(env, napi_create_string_utf8(env, tempName,
3217+
tempNameLength, &queryVars[col].jsName))
3218+
NJS_CHECK_NAPI(env, napi_has_property(env, tempObj,
3219+
queryVars[col].jsName, &exists))
3220+
}
3221+
if (!njsUtils_copyStringFromJS(env, queryVars[col].jsName,
3222+
&queryVars[col].name, &queryVars[col].nameLength))
3223+
return false;
3224+
3225+
NJS_CHECK_NAPI(env, napi_create_uint32(env, col, &colObj))
3226+
NJS_CHECK_NAPI(env, napi_set_property(env, tempObj,
3227+
queryVars[col].jsName, colObj))
3228+
}
3229+
}
3230+
return true;
3231+
}

src/njsModule.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ typedef enum {
219219
#define NJS_SODA_COLL_CREATE_MODE_MAP 5001
220220

221221

222+
// max value used for duplicate name composition (requires space for maximum
223+
// name length (128) and suffix added
224+
#define NJS_MAX_COL_NAME_BUFFER_LENGTH 200
225+
226+
222227
//-----------------------------------------------------------------------------
223228
// forward declarations
224229
//-----------------------------------------------------------------------------

test/dupColNames.js

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. */
2+
3+
/******************************************************************************
4+
*
5+
* You may not use the identified files except in compliance with the Apache
6+
* License, Version 2.0 (the "License.")
7+
*
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
*
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
* The node-oracledb test suite uses 'mocha', 'should' and 'async'.
19+
* See LICENSE.md for relevant licenses.
20+
*
21+
* NAME
22+
* 250. dupColNames.js
23+
*
24+
* DESCRIPTION
25+
* Testcases to detect duplicate column names and suffix for col names
26+
*
27+
*****************************************************************************/
28+
'use strict';
29+
30+
const oracledb = require('oracledb');
31+
const should = require('should');
32+
const dbconfig = require('./dbconfig.js');
33+
34+
// set the outformat to object
35+
oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT;
36+
37+
38+
describe('250. Duplicate Column Names.js', function () {
39+
let connection = null;
40+
const tableNameDept = "nodb_dupDepartment";
41+
const tableNameEmp = "nodb_dupEmployee";
42+
const create_table_sql =
43+
"BEGIN \
44+
DECLARE \
45+
e_table_missing EXCEPTION; \
46+
PRAGMA EXCEPTION_INIT(e_table_missing, -00942); \
47+
BEGIN \
48+
EXECUTE IMMEDIATE ('DROP TABLE " + tableNameDept + " '); \
49+
EXCEPTION \
50+
WHEN e_table_missing \
51+
THEN NULL; \
52+
END; \
53+
EXECUTE IMMEDIATE (' \
54+
CREATE TABLE nodb_dupDepartment ( \
55+
department_id NUMBER, \
56+
department_name VARCHAR2(20) \
57+
) \
58+
'); \
59+
END; ";
60+
const deptInsert = "INSERT INTO " + tableNameDept + " VALUES( :1, :2)";
61+
const create_table_emp_sql =
62+
"BEGIN \n" +
63+
" DECLARE \n" +
64+
" e_table_missing EXCEPTION; \n" +
65+
" PRAGMA EXCEPTION_INIT(e_table_missing, -00942); \n" +
66+
" BEGIN \n" +
67+
" EXECUTE IMMEDIATE('DROP TABLE " + tableNameEmp + " PURGE');\n" +
68+
" EXCEPTION \n" +
69+
" WHEN e_table_missing \n" +
70+
" THEN NULL; \n" +
71+
" END; \n" +
72+
" EXECUTE IMMEDIATE (' \n" +
73+
" CREATE TABLE " + tableNameEmp + " ( \n" +
74+
" employee_id NUMBER, \n" +
75+
" department_id NUMBER, \n" +
76+
" employee_name VARCHAR2(20) \n" +
77+
" ) \n" +
78+
" '); \n" +
79+
" END; ";
80+
const empInsert = "INSERT INTO " + tableNameEmp + " VALUES ( :1, :2, :3) ";
81+
82+
before(async function() {
83+
connection = await oracledb.getConnection (dbconfig);
84+
85+
await connection.execute(create_table_sql );
86+
await connection.execute(deptInsert, [101, "R&D"]);
87+
await connection.execute(deptInsert, [201, "Sales"]);
88+
await connection.execute(deptInsert, [301, "Marketing"]);
89+
90+
await connection.execute(create_table_emp_sql);
91+
await connection.execute(empInsert, [1001, 101, "Krishna Mohan"]);
92+
await connection.execute(empInsert, [1002, 102, "P Venkatraman"]);
93+
await connection.execute(empInsert, [2001, 201, "Chris Jones"]);
94+
await connection.execute(empInsert, [3001, 301, "John Millard"]);
95+
96+
await connection.commit();
97+
});
98+
99+
after(async function() {
100+
await connection.execute("DROP TABLE nodb_dupEmployee PURGE");
101+
await connection.execute("DROP TABLE nodb_dupDepartment PURGE");
102+
await connection.commit();
103+
await connection.close();
104+
});
105+
106+
it('250.1 Two duplicate columns', async function() {
107+
let sql =
108+
`SELECT
109+
A.EMPLOYEE_ID, A.DEPARTMENT_ID,
110+
B.DEPARTMENT_ID, B.DEPARTMENT_NAME
111+
FROM nodb_dupEmployee A, nodb_dupDepartment B
112+
WHERE A.DEPARTMENT_ID = B.DEPARTMENT_ID`;
113+
114+
let result = await connection.execute(sql);
115+
should.equal(result.metaData[0].name, "EMPLOYEE_ID");
116+
should.equal(result.metaData[1].name, "DEPARTMENT_ID");
117+
should.equal(result.metaData[2].name, "DEPARTMENT_ID_1");
118+
should.equal(result.metaData[3].name, "DEPARTMENT_NAME");
119+
});
120+
121+
it('250.2 Three duplicate columns', async function () {
122+
let sql =
123+
`SELECT
124+
A.EMPLOYEE_ID, A.DEPARTMENT_ID,
125+
B.DEPARTMENT_ID, B.DEPARTMENT_ID
126+
FROM nodb_dupEmployee A, nodb_dupDepartment B
127+
WHERE A.DEPARTMENT_ID = B.DEPARTMENT_ID`;
128+
129+
let result = await connection.execute(sql);
130+
should.equal(result.metaData[0].name, "EMPLOYEE_ID");
131+
should.equal(result.metaData[1].name, "DEPARTMENT_ID");
132+
should.equal(result.metaData[2].name, "DEPARTMENT_ID_1");
133+
should.equal(result.metaData[3].name, "DEPARTMENT_ID_2");
134+
});
135+
136+
it('250.3 With conflicting alias name', async function() {
137+
let sql =
138+
`SELECT
139+
A.EMPLOYEE_ID, A.DEPARTMENT_ID,
140+
B.DEPARTMENT_ID, B.DEPARTMENT_ID AS DEPARTMENT_ID_1
141+
FROM nodb_dupEmployee A, nodb_dupDepartment B
142+
WHERE A.DEPARTMENT_ID = B.DEPARTMENT_ID`;
143+
144+
let result = await connection.execute(sql);
145+
should.equal(result.metaData[0].name, "EMPLOYEE_ID");
146+
should.equal(result.metaData[1].name, "DEPARTMENT_ID");
147+
should.equal(result.metaData[2].name, "DEPARTMENT_ID_2");
148+
should.equal(result.metaData[3].name, "DEPARTMENT_ID_1");
149+
});
150+
151+
it('250.4 With non-conflicting alias name', async function () {
152+
let sql =
153+
`SELECT
154+
A.EMPLOYEE_ID, A.DEPARTMENT_ID,
155+
B.DEPARTMENT_ID, B.DEPARTMENT_ID AS DEPARTMENT_ID_5
156+
FROM nodb_dupEmployee A, nodb_dupDepartment B
157+
WHERE A.DEPARTMENT_ID = B.DEPARTMENT_ID`;
158+
159+
let result = await connection.execute(sql);
160+
should.equal(result.metaData[0].name, "EMPLOYEE_ID");
161+
should.equal(result.metaData[1].name, "DEPARTMENT_ID");
162+
should.equal(result.metaData[2].name, "DEPARTMENT_ID_1");
163+
should.equal(result.metaData[3].name, "DEPARTMENT_ID_5");
164+
});
165+
166+
it('250.5 Negative not-case sensitive', async function () {
167+
// alias name is within quotes and so does not match any string
168+
// comparison
169+
let sql =
170+
`SELECT
171+
A.EMPLOYEE_ID, A.DEPARTMENT_ID,
172+
B.department_id, B.department_id AS "department_id_1"
173+
FROM nodb_dupEmployee A, nodb_dupDepartment B
174+
WHERE A.department_id = B.department_id`;
175+
176+
let result = await connection.execute(sql);
177+
should.equal(result.metaData[0].name, "EMPLOYEE_ID");
178+
should.equal(result.metaData[1].name, "DEPARTMENT_ID");
179+
should.equal(result.metaData[2].name, "DEPARTMENT_ID_1");
180+
should.equal(result.metaData[3].name, "department_id_1");
181+
});
182+
183+
});

0 commit comments

Comments
 (0)