index.js

'use strict';

const DBDriver = require('oracledb');
const Stream = require('stream');
const typedefs = require('sqler/typedefs');

/**
 * Oracle database {@link Dialect} implementation for [`sqler`](https://ugate.github.io/sqler/)
 */
class OracleDialect {

  /**
   * Constructor
   * @constructs OracleDialect
   * @param {typedefs.SQLERPrivateOptions} priv The private configuration options
   * @param {OracleConnectionOptions} connConf The individual SQL __connection__ configuration for the given dialect that was passed into the originating {@link Manager}
   * @param {typedefs.SQLERTrack} track Container for sharing data between {@link Dialect} instances.
   * @param {Function} [errorLogger] A function that takes one or more arguments and logs the results as an error (similar to `console.error`)
   * @param {Function} [logger] A function that takes one or more arguments and logs the results (similar to `console.log`)
   * @param {Boolean} [debug] A flag that indicates the dialect should be run in debug mode (if supported)
   */
  constructor(priv, connConf, track, errorLogger, logger, debug) {
    const dlt = internal(this);
    dlt.at.track = track;
    dlt.at.transactions = new Map();
    // sqler compatible state
    dlt.at.state = {
      connections: {
        count: 0,
        inUse: 0
      },
      pending: 0
    };

    dlt.at.driver = DBDriver;

    const hasDrvrOpts = !!connConf.driverOptions;
    const dopts = hasDrvrOpts && connConf.driverOptions.global;
    if (dopts) dlt.at.track.interpolate(dlt.at.driver, dopts);
    // default autoCommit = true to conform to sqler
    dlt.at.driver.autoCommit = true;
    dlt.at.driver.connectionClass = dlt.at.driver.connectionClass || `SqlerOracleGen${Math.floor(Math.random() * 10000)}`;

    /** @type {DBDriver.PoolAttributes} */
    const poolOpts = connConf.pool || {};
    const alias = poolOpts.alias || `sqlerOracleGen${Math.floor(Math.random() * 10000)}`;
    dlt.at.errorLogger = errorLogger;
    dlt.at.logger = logger;
    dlt.at.debug = debug;
    dlt.at.pool = {
      alias,
      conf: poolOpts,
      oracleConf: hasDrvrOpts && connConf.driverOptions.pool ? dlt.at.track.interpolate({}, connConf.driverOptions.pool, dlt.at.driver) : {}
    };
    dlt.at.pingOnInit = hasDrvrOpts && connConf.driverOptions.hasOwnProperty('pingOnInit') ? !!connConf.driverOptions.pingOnInit : true;
    dlt.at.connConf = connConf;

    dlt.at.pool.oracleConf.user = priv.username;
    dlt.at.pool.oracleConf.password = priv.password;
  
    const url = {
      host: connConf.host, // host will be defaulted to priv.host by sqler
      port: connConf.port || priv.port || 1521,
      protocol: connConf.protocol || priv.protocol || 'TCP'
    };
    if (!url.host) throw new Error(`sqler-oracle: Missing ${connConf.dialect} "host" for conection ${connConf.id}/${connConf.name} in private configuration options or connection configuration options`);

    if (connConf.service) {
      if (hasDrvrOpts && connConf.driverOptions.useTNS) {
        //process.env.TNS_ADMIN = priv.privatePath;
        //dlt.at.tns = Path.join(process.env.TNS_ADMIN, 'tnsnames.ora');
        dlt.at.pool.oracleConf.connectString = `(DESCRIPTION = (ADDRESS = (PROTOCOL = ${url.protocol})(HOST = ${url.host})(PORT = ${url.port}))` +
        `(CONNECT_DATA = (SERVER = POOLED)(SERVICE_NAME = ${connConf.service})))`;
        dlt.at.connectionType = 'TNS_SERVICE';
      } else {
        dlt.at.pool.oracleConf.connectString = `${url.host}:${url.port}/${connConf.service}`;
        dlt.at.connectionType = 'SERVICE';
      }
    } else throw new Error(`sqler-oracle: Missing ${connConf.dialect} "service" for conection ${connConf.id}/${connConf.name} in connection configuration options`);
    dlt.at.pool.oracleConf.poolMin = poolOpts.min;
    dlt.at.pool.oracleConf.poolMax = poolOpts.max;
    dlt.at.pool.oracleConf.poolTimeout = poolOpts.idle;
    dlt.at.pool.oracleConf.poolIncrement = poolOpts.increment;
    dlt.at.pool.oracleConf.queueTimeout = poolOpts.timeout;
    dlt.at.pool.oracleConf.poolAlias = alias;
  }

  /**
   * Initializes {@link OracleDialect} by creating the connection pool
   * @param {typedefs.SQLERInitOptions} opts The options described by the `sqler` module
   * @returns {Object} The Oracle connection pool
   */
  async init(opts) {
    const dlt = internal(this), numSql = opts.numOfPreparedFuncs;
    statementCacheSize(dlt, numSql);
    /** @type {InternalFlightRecorder} */
    let recorder;
    /** @type {DBDriver.Pool} */
    let oraPool;
    /** @type {DBDriver.Connection} */
    let conn;
    try {
      try {
        oraPool = dlt.at.driver.getPool(dlt.at.pool.oracleConf.poolAlias);
      } catch(err) {
        // consume error since the pool might not be created yet
      }
      oraPool = oraPool || (await dlt.at.driver.createPool(dlt.at.pool.oracleConf));
      if (dlt.at.logger) {
        dlt.at.logger(`sqler-oracle: ${dlt.at.connectionType} connection pool "${oraPool.poolAlias}" created with poolPingInterval=${oraPool.poolPingInterval} ` +
          `stmtCacheSize=${oraPool.stmtCacheSize} (${numSql} SQL files) poolTimeout=${oraPool.poolTimeout} poolIncrement=${oraPool.poolIncrement} ` +
          `poolMin=${oraPool.poolMin} poolMax=${oraPool.poolMax}`);
      }
      if (dlt.at.pingOnInit) {
        // validate by pinging connection from pool
        conn = await oraPool.getConnection();
        await conn.ping();
      }
      return oraPool;
    } catch (err) {
      recorder = errored(`sqler-oracle: ${oraPool ? 'Unable to ping connection from' : 'Unable to create'} connection pool`, dlt, null, err);
      throw err;
    } finally {
      if (conn) {
        await finalize(recorder, dlt, operation(dlt, 'close', false, conn, opts));
      }
    }
  }

  /**
   * Begins a transaction by opening a connection from the pool
   * @param {String} txId The transaction ID that will be started
   * @param {typedefs.SQLERTransactionOptions} opts The transaction options passed in via the public API
   * @returns {typedefs.SQLERTransaction} The transaction that was started
   */
  async beginTransaction(txId) {
    const dlt = internal(this);
    if (dlt.at.logger) {
      dlt.at.logger(`sqler-oracle: Beginning transaction ${txId} on connection pool "${dlt.at.pool.oracleConf.poolAlias}"`);
    }
    /** @type {typedefs.SQLERTransaction} */
    const tx = {
      id: txId,
      state: Object.seal({
        committed: 0,
        rolledback: 0,
        pending: 0,
        isReleased: false
      })
    };
    const pool = dlt.at.driver.getPool(dlt.at.pool.oracleConf.poolAlias);
    /** @type {OracleTransactionObject} */
    const txo = { tx, conn: await dlt.this.getConnection(pool, dlt.at.connConf) };
    /** @type {typedefs.SQLERExecOptions} */
    const opts = { transactionId: tx.id };
    const commit = operation(dlt, 'commit', true, txo, opts);
    tx.commit = async (isRelease) => {
      await commit();
      if (isRelease) await operation(dlt, 'close', true, txo, opts)();
    };
    const rollback = operation(dlt, 'rollback', true, txo, opts);
    tx.rollback = async (isRelease) => {
      await rollback();
      if (isRelease) await operation(dlt, 'close', true, txo, opts)();
    };
    Object.freeze(tx);
    dlt.at.transactions.set(txId, txo);
    return tx;
  }

  /**
   * Executes a SQL statement
   * @param {String} sql the SQL to execute
   * @param {OracleExecOptions} opts The execution options
   * @param {String[]} frags the frament keys within the SQL that will be retained
   * @param {typedefs.SQLERExecMeta} meta The SQL execution metadata
   * @param {(typedefs.SQLERExecErrorOptions | Boolean)} [errorOpts] The error options to use
   * @returns {typedefs.SQLERExecResults} The execution results
   */
  async exec(sql, opts, frags, meta, errorOpts) {
    /** @type {InternalFlightRecorder} */
    let recorder;
    const dlt = internal(this), numSql = opts.numOfPreparedFuncs;
    statementCacheSize(dlt, numSql); // <- in case it changes from a manager.scan call or the cache expired
    /** @type {OracleTransactionObject} */
    const txo = opts.transactionId ? dlt.at.transactions.get(opts.transactionId) : null;
    /** @type {DBDriver.Connection} */
    let conn;
    /** @type {InternalExecMeta} */
    let execMeta;
    /** @type {DBDriver.Result} */
    let rslts;
    try {
      /** @type {typedefs.SQLERExecResults} */
      const rtn = {};

      // pseudo prepared statement for sqler API compliance
      if (opts.prepareStatement) {
        prepared(dlt, sql, opts, meta, txo, rtn);
      }

      if (opts.stream >= 0) { // streams handle prepared statements when streaming starts
        rslts = [ opts.type === 'READ' ? await createReadStream(dlt, sql, opts, meta, txo, rtn) : createWriteStream(dlt, sql, opts, meta, txo, rtn) ];
        rtn.rows = rslts;
        rtn.raw = rslts;
      } else {
        execMeta = createExecMeta(dlt, sql, opts);
        const pool = dlt.at.driver.getPool(dlt.at.pool.oracleConf.poolAlias);
        conn = txo ? null : await dlt.this.getConnection(pool, opts);
        rslts = await (txo ? txo.conn : conn).execute(execMeta.sql, execMeta.binds, execMeta.dopts.exec);

        if (txo) {
          if (opts.autoCommit) {
            await operation(dlt, 'commit', false, txo, opts)();
          } else {
            txo.tx.state.pending++;
            dlt.at.state.pending++;
          }
        }
        rtn.rows = rslts.rows;
        rtn.raw = rslts;
      }
      return rtn;
    } catch (err) {
      recorder = errored(`sqler-oracle: Failed to execute the following SQL:\n${sql}`, dlt, meta, err);
      throw err;
    } finally {
      if (conn) {
        await finalize(recorder, dlt, operation(dlt, 'close', false, conn, opts));
      }
    }
  }

  /**
   * Gets a new connection from the pool
   * @protected
   * @param {DBDriver.Pool} pool The connection pool
   * @param {OracleExecOptions} [opts] The execution options
   * @returns {DBDriver.Connection} The connection (when present)
   */
  async getConnection(pool, opts) {
    const dlt = internal(this);
    const hasDrvrOpts = opts && !!opts.driverOptions;
    /** @type {DBDriver.PoolAttributes} */
    const poolAttrs = (hasDrvrOpts && opts.driverOptions.pool) || {};
    poolAttrs.poolAlias = dlt.at.pool.oracleConf.poolAlias;
    return pool.getConnection(poolAttrs);
  }

  /**
   * Closes the Oracle connection pool
   * @returns {Number} The number of connections closed
   */
  async close() {
    const dlt = internal(this);
    try {
      /** @type {DBDriver.Pool} */
      let pool;
      try {
        pool = dlt.at.driver.getPool(dlt.at.pool.oracleConf.poolAlias);
      } catch (err) {
      }
      if (dlt.at.logger) {
        dlt.at.logger(`sqler-oracle: Closing connection pool "${dlt.at.pool.oracleConf.poolAlias}" ${statusLabel(dlt)}`);
      }
      if (pool) await pool.close();
      dlt.at.transactions.clear();
      dlt.at.state.pending = 0;
      if (dlt.at.logger) {
        dlt.at.logger(`sqler-oracle: Closed connection pool "${dlt.at.pool.oracleConf.poolAlias}" ${statusLabel(dlt)}`);
      }
      return dlt.at.state.pending;
    } catch (err) {
      errored(`sqler-oracle: Failed to close connection pool "${dlt.at.pool.oracleConf.poolAlias}" ${statusLabel(dlt)}`, dlt, null, err);
      throw err;
    }
  }

  /**
   * @returns {typedefs.SQLERState} The state
   */
  get state() {
    const dlt = internal(this);
    /** @type {DBDriver.Pool} */
    let pooled;
    try {
      pooled = dlt.at.driver.getPool(dlt.at.pool.oracleConf.poolAlias);
    } catch (err) {
      pooled = {};
    }
    dlt.at.state.connections.count = pooled.connectionsOpen || 0;
    dlt.at.state.connections.inUse = pooled.connectionsInUse || 0;
    // use a copy for external use
    return JSON.parse(JSON.stringify(dlt.at.state));
  }

  /**
   * @protected
   * @returns {DBDriver} The `oracledb` driver module
   */
  get driver() {
    return internal(this).at.driver;
  }
}

module.exports = OracleDialect;

/**
 * Creates bind parameters suitable for SQL execution in Oracle
 * @private
 * @param {InternalOracleDB} dlt The internal Oracle object instance
 * @param {String} sql the SQL to execute
 * @param {OracleExecOptions} opts The execution options
 * @param {Object} [bindsAlt] An alternative to `opts.binds` that will be used
 * @returns {InternalExecMeta} The binds metadata
 */
function createExecMeta(dlt, sql, opts, bindsAlt) {
  /** @type {InternalExecMeta} */
  const rtn = {};
  const binds = bindsAlt || opts.binds;

  // interpolate and remove unused binds since
  // Oracle will throw "ORA-01036: illegal variable name/number" when unused bind parameters are passed (also, cuts down on payload bloat)
  rtn.bndp = dlt.at.track.interpolate({}, binds, dlt.at.driver, props => sql.includes(`:${props[0]}`));

  rtn.dopts = opts.driverOptions || {};
  rtn.dopts.exec = !!rtn.dopts && rtn.dopts.exec ? dlt.at.track.interpolate({}, rtn.dopts.exec, dlt.at.driver) : {};
  rtn.dopts.exec.autoCommit = opts.autoCommit;
  if (!rtn.dopts.exec.hasOwnProperty('outFormat')) rtn.dopts.exec.outFormat = dlt.at.driver.OUT_FORMAT_OBJECT;

  rtn.sql = sql;
  rtn.binds = rtn.bndp;

  return rtn;
}

/**
 * Executes a function by name that resides on the Oracle connection
 * @private
 * @param {InternalOracleDB} dlt The internal Oracle object instance
 * @param {String} name The name of the function that will be called on the connection
 * @param {Boolean} [reset] Truthy to reset the pending connection and transaction count when the operation completes successfully
 * @param {(OracleTransactionObject | DBDriver.Connection)} txoOrConn Either the transaction object or the connection itself
 * @param {typedefs.SQLERExecOptions} [opts] The {@link typedefs.SQLERExecOptions}
 * @returns {Function} A no-arguement `async` function that returns the number or pending transactions
 */
function operation(dlt, name, reset, txoOrConn, opts) {
  return async () => {
    /** @type {InternalFlightRecorder} */
    let recorder = {};
    /** @type {OracleTransactionObject} */
    const txo = opts.transactionId && txoOrConn.tx ? txoOrConn : null;
    /** @type {DBDriver.Connection} */
    const conn = txo ? txo.conn : txoOrConn;
    try {
      if (txo && txo.tx.state.isReleased && (name === 'commit' || name === 'rollback')) {
        return Promise.reject(new Error(`"${name}" already called on transaction "${txo.tx.id}"`));
      }
      if (dlt.at.logger) {
        dlt.at.logger(`sqler-oracle: Performing ${name} on connection pool "${dlt.at.pool.oracleConf.poolAlias}" ${statusLabel(dlt, null, txo)}`);
      }
      await conn[name]();
      if (txo) {
        if (name === 'commit') {
          txo.tx.state.committed++;
        } else if (name === 'rollback') {
          txo.tx.state.rolledback++;
        } else if (name === 'close' || name === 'release' /* release is depricated */) {
          txo.tx.state.isReleased = true;
        }
      }
      if (reset) {
        if (txo) dlt.at.transactions.delete(txo.tx.id);
        dlt.at.state.pending = 0;
      }
      if (dlt.at.logger) {
        dlt.at.logger(`sqler-oracle: Performed ${name} on connection pool "${dlt.at.pool.oracleConf.poolAlias}" ${statusLabel(dlt, null, txo)}`);
      }
    } catch (err) {
      recorder = errored(`sqler-oracle: Failed to ${name} ${dlt.at.state.pending} transaction(s) with options: ${
        opts ? JSON.stringify(Object.keys(opts)) : 'N/A'}`, dlt, null, err);
      throw err;
    } finally {
      if (name !== 'close' && name !== 'release' /* release is depricated */ && name !== 'end' && ((recorder && recorder.error) || (!txo && conn))) {
        await finalize(recorder, dlt, operation(dlt, 'close', false, conn, opts));
      }
    }
    return dlt.at.state.pending;
  };
}

/**
 * Returns a label that contains connection details, transaction counts, etc.
 * @private
 * @param {InternalOracleDB} dlt The internal dialect object instance
 * @param {OracleExecOptions} [opts] Execution options that will be included in the staus label
 * @param {OracleTransactionObject} [txo] An optional transactiopn to add to the status label
 * @returns {String} The status label
 */
function statusLabel(dlt, opts, txo) {
  try {
    const state = dlt.at.state;
    return `(( ${opts ? `[ ${opts.name ? `name: ${opts.name}, ` : ''}type: ${opts.type} ]` : ''}[ uncommitted transactions: ${state.pending}${
      dlt.at.pool ? `, total connections: ${state.connections.count}, active connections: ${state.connections.inUse}` : ''} ]${
        txo ? ` - Transaction state: ${JSON.stringify(txo.tx.state)}` : ''} ))`;
  } catch (err) {
    if (dlt.at.errorLogger) {
      dlt.at.errorLogger('sqler-oracle: Failed to create status label', err);
    }
  }
}

/**
 * Creates a read stream that batches the read SQL executions
 * @private
 * @param {InternalOracleDB} dlt The internal Oracle object instance
 * @param {String} sql The SQL to execute.
 * @param {OracleExecOptions} opts The execution options
 * @param {typedefs.SQLERExecMeta} meta The SQL execution metadata
 * @param {OracleTransactionObject} [txo] The transaction object to use. When not specified, a connection will be established on the first write to the stream.
 * @param {typedefs.SQLERExecResults} rtn Where the _public_ prepared statement functions will be set (ignored when the read stream is not for a prepared
 * statement).
 * @returns {Stream.Readable} The created read stream
 */
async function createReadStream(dlt, sql, opts, meta, txo, rtn) {
  /** @type {Promise<DBDriver.Connection>} */
  let connProm;
  /** @type {InternalFlightRecorder[]} */
  const recorders = [];
  const execMeta = createExecMeta(dlt, sql, opts);
  const pool = dlt.at.driver.getPool(dlt.at.pool.oracleConf.poolAlias);
  const conn = txo ? null : connProm ? await connProm : await (connProm = dlt.this.getConnection(pool, opts));
  const readable = (txo ? txo.conn : conn).queryStream(execMeta.sql, execMeta.binds, execMeta.dopts.exec);
  // dlt.at.track.readable(opts, readable);
  readable.on('error', async (err) => {
    if (err.sqlerOracle) return;
    recorders.push(errored(`sqler-oracle: An error occurred during ${Stream.Readable.name} streaming for SQL:\n${sql}`, dlt, meta, err));
  });
  readable.on('close', closeStreamHandler(dlt, sql, opts, meta, txo, () => connProm, readable, recorders));
  return readable;
}

/**
 * Creates a write stream that batches the write SQL executions
 * @private
 * @param {InternalOracleDB} dlt The internal Oracle object instance
 * @param {String} sql The SQL to execute
 * @param {OracleExecOptions} opts The execution options
 * @param {typedefs.SQLERExecMeta} meta The SQL execution metadata
 * @param {OracleTransactionObject} [txo] The transaction object to use. When not specified, a connection will be established on the first write to the stream.
 * @param {typedefs.SQLERExecResults} rtn Where the _public_ prepared statement functions will be set (ignored when the write stream is not for a prepared
 * statement).
 * @returns {Stream.Writable} The created write stream
 */
function createWriteStream(dlt, sql, opts, meta, txo, rtn) {
  /** @type {Promise<DBDriver.Connection>} */
  let connProm;
  /** @type {InternalFlightRecorder[]} */
  const recorders = [];
  const writable = dlt.at.track.writable(opts, async (batch) => {
    try {
      if (dlt.at.logger) {
        dlt.at.logger(`sqler-oracle: Started ${Stream.Writable.name} stream execution for ${batch.length} batches ${statusLabel(dlt, opts, txo)}`);
      }
      const pool = dlt.at.driver.getPool(dlt.at.pool.oracleConf.poolAlias);
      const conn = txo ? txo.conn : connProm ? await connProm : await (connProm = dlt.this.getConnection(pool, opts));
      // batch all the binds into a single exectuion for a performance gain
      // https://oracle.github.io/node-oracledb/doc/api.html see connection.executeMany()
      let rslts, rslt;
      let bi = 0;
      const bindsArray = new Array(batch.length);
      /** @type {InternalExecMeta} */
      let execMeta;
      for (let binds of batch) {
        execMeta = createExecMeta(dlt, sql, opts, binds);
        // rslt = await conn.execute(execMeta.sql, execMeta.binds, execMeta.dopts.exec);
        // if (rslts) rslts.push(rslt);
        // else rslts = [ rslt ];
        bindsArray[bi] = execMeta.binds;
        bi++;
      }
      rslt = await conn.executeMany(execMeta.sql, bindsArray, execMeta.dopts.exec);
      if (rslts) {
        rslts.push(rslt);
      } else {
        rslts = Array.isArray(rslt) ? rslt : [ rslt ];
      }
      if (dlt.at.logger) {
        dlt.at.logger(`sqler-oracle: Completed execution of ${batch.length} batched write streams${
          execMeta.outs ? ` (with ${execMeta.outs.length} out binds)` : ''}`);
      }
      return rslts;
    } catch (err) {
      recorders.push(errored(`sqler-oracle: Failed to execute writable stream batch for ${
        batch ? batch.length : 'invalid batch'} on the following SQL:\n${sql}`, dlt, meta, err));
      throw err;
    }
  });
  writable.on('close' /* 'finish' */, closeStreamHandler(dlt, sql, opts, meta, txo, () => connProm, writable, recorders));
  return writable;
}

/**
 * Handles a `close` event on a stream by closing a connection (when passed), emitting the {@link typedefs.EVENT_STREAM_RELEASE} event and handling a transaction
 * `commit` (when a {@link OracleTransactionObject} is passed).
 * @private
 * @param {InternalOracleDB} dlt The internal dialect object instance
 * @param {String} sql The SQL to execute
 * @param {OracleExecOptions} opts The execution options
 * @param {typedefs.SQLERExecMeta} meta The SQL execution metadata
 * @param {OracleTransactionObject} [txo] The transaction object to use. When not specified, a connection will be established on the first write to the stream.
 * @param {Function} [getConn] An `async function()` to get the {@link DBDriver.Connection} that will be closed (ignored when a transaction is specified).
 * @param {(Stream.Readable | Stream.Writable)} stream The stream where the `close` event will be emitted.
 * @param {InternalFlightRecorder[]} recorders The flight recorders where the any errors will be recorded.
 * @returns {Function} An `async function()` that handles the `close` event on the specified stream
 */
function closeStreamHandler(dlt, sql, opts, meta, txo, getConn, stream, recorders) {
  const type = stream instanceof Stream.Readable ? Stream.Readable.name : stream instanceof Stream.Writable ? Stream.Writable.name : 'N/A';
  let isCommitted;
  return async () => {
    try {
      /** @type {DBDriver.Connection} */
      const conn = typeof getConn === 'function' ? await getConn() : null;
      if (conn) {
        await operation(dlt, 'close', false, conn, opts)();
        stream.emit(typedefs.EVENT_STREAM_RELEASE);
      }
      if (txo && opts.autoCommit && !recorders.length) {
        await operation(dlt, 'commit', false, txo, opts)();
        isCommitted = true;
        stream.emit(typedefs.EVENT_STREAM_COMMIT, txo.tx.id);
      } else if (txo) {
        txo.tx.state.pending++;
        dlt.at.state.pending++;
      }
    } catch (err) {
      recorders.push(errored(`sqler-oracle: Failed to handle ${type} stream close event for SQL:\n${sql}`, dlt, meta, err));
      stream.emit('error', recorders[recorders.length - 1].error);
    } finally {
      if (!isCommitted && txo && opts.autoCommit && recorders.length) {
        await finalize(recorders, dlt, async () => {
          await operation(dlt, 'rollback', false, txo, opts)();
          stream.emit(typedefs.EVENT_STREAM_ROLLBACK, txo.tx.id);
        });
      }
    }
  };
}

/**
 * Either generates a prepared statement when it doesn't currently exist, or returns an existing prepared statement that waits for the original prepared statement
 * creation/connectivity/setup to complete before performing any executions.
 * @private
 * @param {InternalOracleDB} dlt The internal Oracle object instance
 * @param {String} sql The raw SQL to execute for the prepared statement
 * @param {OracleExecOptions} opts The execution options
 * @param {typedefs.SQLERExecMeta} meta The SQL execution metadata
 * @param {OracleTransactionObject} [txo] The transaction object to use. When not specified, a connection will be established.
 * @param {typedefs.SQLERExecResults} rtn The execution results used by the prepared statement where `unprepare` will be set
 * @returns {Object} The prepared statement
 */
function prepared(dlt, sql, opts, meta, txo, rtn) {
  rtn.unprepare = async () => {
    if (dlt.at.logger) {
      dlt.at.logger(`sqler-oracle: "unprepare" is a noop since Oracle implements the concept of statement caching instead (${
        meta.path
      }). See https://oracle.github.io/node-oracledb/doc/api.html#stmtcache`);
    }
  };
  return rtn;
}

/**
 * There is no prepare/unprepare since Oracle uses {@link https://oracle.github.io/node-oracledb/doc/api.html#stmtcache statement caching}.
 * Statement cache should account for the number of prepared functions/SQL files by a factor of `3x` to accomodate that many fragments in each SQL file.
 * @private
 * @param {InternalOracleDB} dlt The internal Oracle object instance
 * @param {Number} numSql The total number of SQL files used on the dialect
 * @returns {Number} The statement cache size
 */
function statementCacheSize(dlt, numSql) {
  dlt.at.pool.oracleConf.stmtCacheSize = (dlt.at.driverOptions && dlt.at.driverOptions.stmtCacheSize) || ((numSql || 1) * 3);
  return dlt.at.pool.oracleConf.stmtCacheSize;
}

/**
 * Error handler
 * @private
 * @param {String} label A label to use to describe the error
 * @param {InternalOracleDB} dlt The internal dialect object instance
 * @param {typedefs.SQLERExecMeta} [meta] The SQL execution metadata
 * @param {Error} error An error that has occurred
 * @returns {InternalFlightRecorder} The flight recorder
 */
function errored(label, dlt, meta, error) {
  if (dlt.at.errorLogger) {
    dlt.at.errorLogger(label, error);
  }
  try {
    const pconf = Object.assign({}, dlt.at.pool.oracleConf);
    pconf.password = '***'; // mask sensitive data
    error.sqlerOracle = {
      message: label,
      poolConf: pconf,
      status: statusLabel(dlt)
    };
  } catch (err) {
    if (dlt.at.errorLogger) {
      dlt.at.errorLogger('sqler-oracle: Failed to capture error meta', err);
    }
  }
  return { error };
}

/**
 * Finally block handler
 * @private
 * @param {(InternalFlightRecorder | InternalFlightRecorder[])} [recorder] The flight recorder
 * @param {InternalOracleDB} dlt The internal dialect object instance
 * @param {Function} [func] An `async function()` that will be invoked in a catch wrapper that will be consumed and recorded when a flight recorder is
 * provided
 * @param {String} [funcErrorProperty=releaseError] A property name on the flight recorder error that will be set when the `func` itself errors
 * @returns {InternalFlightRecorder} The recorded error
 */
async function finalize(recorder, dlt, func, funcErrorProperty = 'releaseError') {
  // transactions/prepared statements need the connection to remain open until commit/rollback/unprepare
  if (typeof func === 'function') {
    try {
      await func();
    } catch (err) {
      if (recorder) {
        for (let rec of Array.isArray(recorder) ? recorder : [recorder]) {
          if (rec.error) recorder.error[funcErrorProperty] = err;
        }
      }
    }
  }
}

// private mapping
let map = new WeakMap();

/**
 * Internal state generator
 * @private
 * @param {DBDriver} dialect The dialect driver
 * @returns {InternalOracleDB} TThe internal dialect state
 */
let internal = function(dialect) {
  if (!map.has(dialect)) {
    map.set(dialect, {});
  }
  return {
    at: map.get(dialect),
    this: dialect
  };
};

/**
 * Oracle specific driver options
 * @typedef {Object} OracleDriverOptions
 * @property {Object} [global] An object that will contain properties set on the global `oracledb` module class. When a value is a string surrounded by `${}`,
 * it will be assumed to be a _constant_ property that resides on the `oracledb` module and will be interpolated accordingly.
 * For example `driverOptions.global.someProp = '${ORACLEDB_CONSTANT}'` will be interpolated as `oracledb.someProp = oracledb.ORACLEDB_CONSTANT`.
 * @property {Object} [pool] The pool `conf` options that will be passed into `oracledb.createPool({ conf })`. __Using any of the generic `pool.someOption`
 * will override the `conf` options set on `driverOptions.pool`.__ When a value is a string surrounded by `${}`, it will be assumed to be a _constant_
 * property that resides on the `oracledb` module and will be interpolated accordingly.
 * For example `driverOptions.pool.someProp = '${ORACLEDB_CONSTANT}'` will be interpolated as `pool.someProp = oracledb.ORACLEDB_CONSTANT`.
 * @property {Boolean} [pingOnInit=true] A truthy flag that indicates if a _ping_ will be performed after the connection pool is created when
 * {@link OracleDialect.init} is called.
 * @property {Boolean} [useTNS] Truthy to build a TNS `sql*net` connection string
 * @property {Number} [stmtCacheSize=numberOfSQLFiles * 3] The statement size that `oracledb` uses
 */

/**
 * Oracle specific extension of the {@link typedefs.SQLERConnectionOptions} from the [`sqler`](https://ugate.github.io/sqler/) module.
 * @typedef {Object} OracleConnectionOptionsType
 * @property {OracleDriverOptions} [driverOptions] The `oracledb` module specific options.
 * @typedef {typedefs.SQLERConnectionOptions & OracleConnectionOptionsType} OracleConnectionOptions
 */

/**
 * Oracle specific extension of the execution options
 * @typedef {Object} OracleExecDriverOptions
 * @property {DBDriver.Pool} [pool] The pool attribute options passed into `oracledbPool.getConnection()`. When a value is a string surrounded by `${}`, it will be assumed
 * to be a _constant_ property that resides on the `oracledb` module and will be interpolated accordingly.
 * For example `driverOptions.pool.someProp = '${ORACLEDB_CONSTANT}'` will be interpolated as `pool.someProp = oracledb.ORACLEDB_CONSTANT`.
 * @property {(DBDriver.ExecuteOptions | DBDriver.ExecuteManyOptions)} [exec] The execution options passed into `oracledbConnection.execute()`.
 * __NOTE: `driverOptions.autoCommit` is ignored in favor of the universal `autoCommit` set directly on the {@link typedefs.SQLERExecOptions}.__
 * When a value is a string surrounded by `${}`, it will be assumed to be a _constant_ property that resides on the `oracledb` module and will be interpolated
 * accordingly.
 * For example `driverOptions.exec.someProp = '${ORACLEDB_CONSTANT}'` will be interpolated as `oracledbExecOpts.someProp = oracledb.ORACLEDB_CONSTANT`.
 * When streaming __writes__ using `execOpts.stream`, all executions are batched into `oracledb.Connection.executeMany` using `execOpts.stream` value as the batch size.
 * Therefore, a valid `exec.bindDefs` should be included as defined within the [oracldb documentation](https://oracle.github.io/node-oracledb/doc/api.html) when [DML
 * RETURNING](https://oracle.github.io/node-oracledb/doc/api.html#dml-returning-with-executemany).
 */

/**
 * Oracle specific extension of the {@link typedefs.SQLERExecOptions} from the [`sqler`](https://ugate.github.io/sqler/) module. When a property of `binds` contains
 * an object it will be _interpolated_ for property values on the `oracledb` module.
 * For example, `binds.name = { dir: '${BIND_OUT}', type: '${STRING}', maxSize: 40 }` will be interpolated as
 * `binds.name = { dir: oracledb.BIND_OUT, type: oracledb.STRING, maxSize: 40 }`.
 * @typedef {Object} OracleExecOptionsType
 * @property {OracleExecDriverOptions} [driverOptions] The `oracledb` module specific execution options.
 * @typedef {typedefs.SQLERExecOptions & OracleExecOptionsType} OracleExecOptions
 */

/**
 * Transactions are wrapped in a parent transaction object so private properties can be added (e.g. prepared statements)
 * @typedef {Object} OracleTransactionObject
 * @property {typedefs.SQLERTransaction} tx The transaction
 * @property {DBDriver.Connection} conn The connection
 * @property {Map<String, Function>} unprepares Map of prepared statement names (key) and no-argument _async_ functions that will be called as a pre-operation
 * call prior to `commit` or `rollback` (value)
 */

// ========================================== Internal Use ==========================================

/**
 * Internal database use
 * @typedef {Object} InternalOracleDB
 * @property {OracleDialect} this The dialect instance
 * @property {Object} at The internal dialect state
 * @property {typedefs.SQLERTrack} at.track The track
 * @property {DBDriver} at.driver The dialect driver
 * @property {Map<String, OracleTransactionObject>} at.transactions The transactions map
 * @property {Object} at.pool The connection pool
 * @property {DBDriver.PoolAttributes} at.pool.oracleConf The `oracledb` pool attributes
 * @property {String} at.pool.alias The alias for the pool used to lookup the pool
 * @property {DBDriver.PoolAttributes} at.pool.conf The original/raw pool attributes
 * @property {typedefs.SQLERState} at.state The __global__ dialect state
 * @property {Function} [at.errorLogger] A function that takes one or more arguments and logs the results as an error (similar to `console.error`)
 * @property {Function} [at.logger] A function that takes one or more arguments and logs the results (similar to `console.log`)
 * @property {Boolean} [at.debug] A flag that indicates the dialect should be run in debug mode (if supported)
 * @property {Boolean} [at.pingOnInit] Truthy to ping when {@link OracleDialect.init}
 * @private
 */

/**
 * Metadata used inpreparation for execution.
 * @typedef {Object} InternalExecMeta
 * @property {OracleExecDriverOptions} dopts The formatted execution driver options.
 * @property {String} sql The formatted/bound execution SQL statement. Will also be set on `dopts.exec.sql` (when present).
 * @property {(Object | Array)} [binds] Either an object that contains the bind parameters as property names and property values as the bound values that can be
 * bound to an SQL statement or an `Array` of values format to support use of `?` parameter markers (non-prepared statements).
 * @property {Object} [bndp] The interpolated version of `opts.binds`.
 * @property {Object} [bindDefs] The properties within `binds` that are marked to be `oracledb.BIND_OUT`
 * @private
 */

/**
 * @typedef {Object} InternalFlightRecorder
 * @property {Error} [error] An errored that occurred
 * @property {DBDriver.Connection} [conn] A connection that will be `released` when an error exists
 * @private
 */

3.0.0 (2021-08-26)

Full Changelog

Features:

Fixes: