Skip to content

Commit

Permalink
Support Object Names Greater than 30 Characters for Oracle DB Version…
Browse files Browse the repository at this point in the history
…s 12.2 and Greater (#5197)



Co-authored-by: Drew Royster <drew.124@gmail.com>
Co-authored-by: Kendra Bryant <kendraa@utahcounty.gov>
Co-authored-by: Matt Bailey <bailey.matthewr@gmail.com>
  • Loading branch information
4 people committed Oct 16, 2023
1 parent 3ba9550 commit 82f43d5
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 71 deletions.
4 changes: 3 additions & 1 deletion lib/dialects/oracle/schema/internal/incrementUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ const Trigger = require('./trigger');

// helper function for pushAdditional in increments() and bigincrements()
function createAutoIncrementTriggerAndSequence(columnCompiler) {
const trigger = new Trigger(columnCompiler.client.version);

// TODO Add warning that sequence etc is created
columnCompiler.pushAdditional(function () {
const tableName = this.tableCompiler.tableNameRaw;
const schemaName = this.tableCompiler.schemaNameRaw;
const createTriggerSQL = Trigger.createAutoIncrementTrigger(
const createTriggerSQL = trigger.createAutoIncrementTrigger(
this.client.logger,
tableName,
schemaName
Expand Down
56 changes: 38 additions & 18 deletions lib/dialects/oracle/schema/internal/trigger.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
const utils = require('../../utils');
const { NameHelper } = require('../../utils');

const trigger = {
renameColumnTrigger: function (logger, tableName, columnName, to) {
const triggerName = utils.generateCombinedName(
class Trigger {
constructor(oracleVersion) {
this.nameHelper = new NameHelper(oracleVersion);
}

renameColumnTrigger(logger, tableName, columnName, to) {
const triggerName = this.nameHelper.generateCombinedName(
logger,
'autoinc_trg',
tableName
);
const sequenceName = utils.generateCombinedName(logger, 'seq', tableName);
const sequenceName = this.nameHelper.generateCombinedName(
logger,
'seq',
tableName
);
return (
`DECLARE ` +
`PK_NAME VARCHAR(200); ` +
Expand Down Expand Up @@ -41,19 +49,19 @@ const trigger = {
` end if;` +
`END;`
);
},
}

createAutoIncrementTrigger: function (logger, tableName, schemaName) {
createAutoIncrementTrigger(logger, tableName, schemaName) {
const tableQuoted = `"${tableName}"`;
const tableUnquoted = tableName;
const schemaQuoted = schemaName ? `"${schemaName}".` : '';
const constraintOwner = schemaName ? `'${schemaName}'` : 'cols.owner';
const triggerName = utils.generateCombinedName(
const triggerName = this.nameHelper.generateCombinedName(
logger,
'autoinc_trg',
tableName
);
const sequenceNameUnquoted = utils.generateCombinedName(
const sequenceNameUnquoted = this.nameHelper.generateCombinedName(
logger,
'seq',
tableName
Expand Down Expand Up @@ -86,17 +94,29 @@ const trigger = {
` end;'); ` +
`END;`
);
},
}

renameTableAndAutoIncrementTrigger: function (logger, tableName, to) {
const triggerName = utils.generateCombinedName(
renameTableAndAutoIncrementTrigger(logger, tableName, to) {
const triggerName = this.nameHelper.generateCombinedName(
logger,
'autoinc_trg',
tableName
);
const sequenceName = utils.generateCombinedName(logger, 'seq', tableName);
const toTriggerName = utils.generateCombinedName(logger, 'autoinc_trg', to);
const toSequenceName = utils.generateCombinedName(logger, 'seq', to);
const sequenceName = this.nameHelper.generateCombinedName(
logger,
'seq',
tableName
);
const toTriggerName = this.nameHelper.generateCombinedName(
logger,
'autoinc_trg',
to
);
const toSequenceName = this.nameHelper.generateCombinedName(
logger,
'seq',
to
);
return (
`DECLARE ` +
`PK_NAME VARCHAR(200); ` +
Expand Down Expand Up @@ -129,7 +149,7 @@ const trigger = {
` end if;` +
`END;`
);
},
};
}
}

module.exports = trigger;
module.exports = Trigger;
6 changes: 4 additions & 2 deletions lib/dialects/oracle/schema/oracle-compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ class SchemaCompiler_Oracle extends SchemaCompiler {

// Rename a table on the schema.
renameTable(tableName, to) {
const renameTable = Trigger.renameTableAndAutoIncrementTrigger(
const trigger = new Trigger(this.client.version);
const renameTable = trigger.renameTableAndAutoIncrementTrigger(
this.client.logger,
tableName,
to
Expand Down Expand Up @@ -60,7 +61,8 @@ class SchemaCompiler_Oracle extends SchemaCompiler {

_dropRelatedSequenceIfExists(tableName) {
// removing the sequence that was possibly generated by increments() column
const sequenceName = utils.generateCombinedName(
const nameHelper = new utils.NameHelper(this.client.version);
const sequenceName = nameHelper.generateCombinedName(
this.client.logger,
'seq',
tableName
Expand Down
11 changes: 9 additions & 2 deletions lib/dialects/oracle/schema/oracle-tablecompiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ class TableCompiler_Oracle extends TableCompiler {
renameColumn(from, to) {
// Remove quotes around tableName
const tableName = this.tableName().slice(1, -1);
const trigger = new Trigger(this.client.version);
return this.pushQuery(
Trigger.renameColumnTrigger(this.client.logger, tableName, from, to)
trigger.renameColumnTrigger(this.client.logger, tableName, from, to)
);
}

Expand Down Expand Up @@ -86,8 +87,14 @@ class TableCompiler_Oracle extends TableCompiler {
}

_indexCommand(type, tableName, columns) {
const nameHelper = new utils.NameHelper(this.client.version);
return this.formatter.wrap(
utils.generateCombinedName(this.client.logger, type, tableName, columns)
nameHelper.generateCombinedName(
this.client.logger,
type,
tableName,
columns
)
);
}

Expand Down
63 changes: 41 additions & 22 deletions lib/dialects/oracle/utils.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
function generateCombinedName(logger, postfix, name, subNames) {
const crypto = require('crypto');
const limit = 30;
if (!Array.isArray(subNames)) subNames = subNames ? [subNames] : [];
const table = name.replace(/\.|-/g, '_');
const subNamesPart = subNames.join('_');
let result = `${table}_${
subNamesPart.length ? subNamesPart + '_' : ''
}${postfix}`.toLowerCase();
if (result.length > limit) {
logger.warn(
`Automatically generated name "${result}" exceeds ${limit} character ` +
`limit for Oracle. Using base64 encoded sha1 of that name instead.`
);
// generates the sha1 of the name and encode it with base64
result = crypto
.createHash('sha1')
.update(result)
.digest('base64')
.replace('=', '');
class NameHelper {
constructor(oracleVersion) {
this.oracleVersion = oracleVersion;

// In oracle versions prior to 12.2, the maximum length for a database
// object name was 30 characters. 12.2 extended this to 128.
const versionParts = oracleVersion
.split('.')
.map((versionPart) => parseInt(versionPart));
if (
versionParts[0] > 12 ||
(versionParts[0] === 12 && versionParts[1] >= 2)
) {
this.limit = 128;
} else {
this.limit = 30;
}
}

generateCombinedName(logger, postfix, name, subNames) {
const crypto = require('crypto');
if (!Array.isArray(subNames)) subNames = subNames ? [subNames] : [];
const table = name.replace(/\.|-/g, '_');
const subNamesPart = subNames.join('_');
let result = `${table}_${
subNamesPart.length ? subNamesPart + '_' : ''
}${postfix}`.toLowerCase();
if (result.length > this.limit) {
logger.warn(
`Automatically generated name "${result}" exceeds ${this.limit} character ` +
`limit for Oracle Database ${this.oracleVersion}. Using base64 encoded sha1 of that name instead.`
);
// generates the sha1 of the name and encode it with base64
result = crypto
.createHash('sha1')
.update(result)
.digest('base64')
.replace('=', '');
}
return result;
}
return result;
}

function wrapSqlWithCatch(sql, errorNumberToCatch) {
Expand Down Expand Up @@ -80,7 +99,7 @@ function isConnectionError(err) {
}

module.exports = {
generateCombinedName,
NameHelper,
isConnectionError,
wrapSqlWithCatch,
ReturningHelper,
Expand Down
86 changes: 70 additions & 16 deletions lib/dialects/oracledb/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ const { compileCallback } = require('../../formatter/formatterUtils');
class Client_Oracledb extends Client_Oracle {
constructor(config) {
super(config);

if (this.version) {
// Normalize version format; null bad format
// to trigger fallback to auto-detect.
this.version = parseVersion(this.version);
}

if (this.driver) {
process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || 1;
process.env.UV_THREADPOOL_SIZE =
Expand Down Expand Up @@ -120,43 +127,40 @@ class Client_Oracledb extends Client_Oracle {
// Get a raw connection, called by the `pool` whenever a new
// connection needs to be added to the pool.
acquireRawConnection() {
const client = this;
const asyncConnection = new Promise(function (resolver, rejecter) {
return new Promise((resolver, rejecter) => {
// If external authentication don't have to worry about username/password and
// if not need to set the username and password
const oracleDbConfig = client.connectionSettings.externalAuth
? { externalAuth: client.connectionSettings.externalAuth }
const oracleDbConfig = this.connectionSettings.externalAuth
? { externalAuth: this.connectionSettings.externalAuth }
: {
user: client.connectionSettings.user,
password: client.connectionSettings.password,
user: this.connectionSettings.user,
password: this.connectionSettings.password,
};

// In the case of external authentication connection string will be given
oracleDbConfig.connectString = resolveConnectString(
client.connectionSettings
this.connectionSettings
);

if (client.connectionSettings.prefetchRowCount) {
oracleDbConfig.prefetchRows =
client.connectionSettings.prefetchRowCount;
if (this.connectionSettings.prefetchRowCount) {
oracleDbConfig.prefetchRows = this.connectionSettings.prefetchRowCount;
}

if (client.connectionSettings.stmtCacheSize !== undefined) {
oracleDbConfig.stmtCacheSize = client.connectionSettings.stmtCacheSize;
if (this.connectionSettings.stmtCacheSize !== undefined) {
oracleDbConfig.stmtCacheSize = this.connectionSettings.stmtCacheSize;
}

client.driver.fetchAsString = client.fetchAsString;
this.driver.fetchAsString = this.fetchAsString;

client.driver.getConnection(oracleDbConfig, function (err, connection) {
this.driver.getConnection(oracleDbConfig, (err, connection) => {
if (err) {
return rejecter(err);
}
monkeyPatchConnection(connection, client);
monkeyPatchConnection(connection, this);

resolver(connection);
});
});
return asyncConnection;
}

// Used to explicitly close a connection, called internally by the pool
Expand All @@ -165,6 +169,39 @@ class Client_Oracledb extends Client_Oracle {
return connection.release();
}

// Handle oracle version resolution on acquiring connection from pool instead of connection creation.
// Must do this here since only the client used to create a connection would be updated with version
// information on creation. Poses a problem when knex instance is cloned since instances share the
// connection pool while having their own client instances.
async acquireConnection() {
const connection = await super.acquireConnection();
this.checkVersion(connection);
return connection;
}

// In Oracle, we need to check the version to dynamically determine
// certain limits. If user did not specify a version, get it from the connection.
checkVersion(connection) {
// Already determined version before?
if (this.version) {
return this.version;
}

const detectedVersion = parseVersion(connection.oracleServerVersionString);
if (!detectedVersion) {
// When original version is set to null, user-provided version was invalid and we fell-back to auto-detect.
// Otherwise, we couldn't auto-detect at all. Set error message accordingly.
throw new Error(
this.version === null
? 'Invalid Oracledb version number format passed to knex. Unable to successfully auto-detect as fallback. Please specify a valid oracledb version.'
: 'Unable to detect Oracledb version number automatically. Please specify the version in knex configuration.'
);
}

this.version = detectedVersion;
return detectedVersion;
}

// Runs the query on the specified connection, providing the bindings
// and any other necessary prep work.
_query(connection, obj) {
Expand Down Expand Up @@ -300,12 +337,29 @@ class Client_Oracledb extends Client_Oracle {
}

processPassedConnection(connection) {
this.checkVersion(connection);
monkeyPatchConnection(connection, this);
}
}

Client_Oracledb.prototype.driverName = 'oracledb';

function parseVersion(versionString) {
try {
// We only care about first two version components at most
const versionParts = versionString.split('.').slice(0, 2);
// Strip off any character suffixes in version number (ex. 12c => 12, 12.2c => 12.2)
versionParts.forEach((versionPart, idx) => {
versionParts[idx] = versionPart.replace(/\D$/, '');
});
const version = versionParts.join('.');
return version.match(/^\d+\.?\d*$/) ? version : null;
} catch (err) {
// Non-string versionString passed in.
return null;
}
}

function resolveConnectString(connectionSettings) {
if (connectionSettings.connectString) {
return connectionSettings.connectString;
Expand Down
4 changes: 4 additions & 0 deletions test/db-less-test-suite.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ if (config.postgres) {
require('./unit/dialects/postgres');
}

if (config.oracledb) {
require('./unit/dialects/oracledb');
}

if (config.mysql) {
require('./unit/dialects/mysql');
}
Expand Down
Loading

0 comments on commit 82f43d5

Please sign in to comment.