From 3ff1598ed95f9250965d03ddcdaa4fac24506fcc Mon Sep 17 00:00:00 2001 From: Sudarshan Soma <48428602+sudarshan12s@users.noreply.github.com> Date: Tue, 14 Jun 2022 15:22:19 +0530 Subject: [PATCH 01/14] feat(oracle): add oracle dialect support (#1) * feat(oracle): add oracle dialect support --- .github/workflows/ci.yml | 27 + dev/oracle/latest/docker-compose.yml | 17 + dev/oracle/latest/privileges.sql | 27 + dev/oracle/latest/start.sh | 44 + dev/oracle/latest/stop.sh | 10 + dev/oracle/latest/wait-until-healthy.sh | 23 + package.json | 11 +- src/data-types.js | 1 + src/dialects/abstract/query-generator.js | 57 +- src/dialects/oracle/connection-manager.js | 192 +++ src/dialects/oracle/data-types.js | 470 ++++++ src/dialects/oracle/index.js | 66 + src/dialects/oracle/query-generator.js | 1443 +++++++++++++++++ src/dialects/oracle/query-interface.js | 87 + src/dialects/oracle/query.js | 672 ++++++++ src/model.js | 2 +- src/sequelize.js | 12 +- src/sql-string.js | 9 +- src/utils.js | 2 +- test/config/config.js | 12 + .../associations/belongs-to-many.test.js | 6 +- .../associations/belongs-to.test.js | 3 +- .../integration/associations/has-many.test.js | 2 +- test/integration/associations/has-one.test.js | 3 +- test/integration/cls.test.js | 6 +- test/integration/configuration.test.js | 7 +- test/integration/data-types.test.js | 76 +- test/integration/error.test.js | 2 +- test/integration/include.test.js | 5 + test/integration/include/findAll.test.js | 13 +- test/integration/include/schema.test.js | 15 +- test/integration/instance/values.test.js | 2 + test/integration/json.test.js | 5 +- test/integration/model.test.js | 49 +- .../model/attributes/field.test.js | 5 + .../model/attributes/types.test.js | 2 + test/integration/model/bulk-create.test.js | 2 +- test/integration/model/create.test.js | 2 +- test/integration/model/findAll/order.test.js | 4 +- test/integration/pool.test.js | 15 +- test/integration/query-interface.test.js | 34 +- .../query-interface/changeColumn.test.js | 6 +- .../query-interface/createTable.test.js | 3 +- .../query-interface/describeTable.test.js | 8 + test/integration/sequelize.test.js | 9 +- .../integration/sequelize.transaction.test.js | 3 + test/integration/sequelize/query.test.js | 115 +- test/integration/sequelize/truncate.test.js | 2 +- test/integration/timezone.test.js | 5 + test/integration/transaction.test.js | 16 +- test/integration/utils.test.js | 4 +- test/support.js | 4 + .../unit/dialect-module-configuration.test.js | 1 + .../dialects/abstract/query-generator.test.js | 3 + test/unit/esm-named-exports.test.js | 1 + test/unit/query-interface/bulk-insert.test.js | 3 +- test/unit/query-interface/raw-select.test.js | 1 + test/unit/query-interface/select.test.js | 1 + test/unit/sql/add-constraint.test.js | 7 +- test/unit/sql/change-column.test.js | 2 + test/unit/sql/create-table.test.js | 3 + test/unit/sql/data-types.test.js | 150 +- test/unit/sql/delete.test.js | 5 + test/unit/sql/generateJoin.test.js | 38 +- test/unit/sql/group.test.js | 2 + test/unit/sql/index.test.js | 4 + test/unit/sql/insert.test.js | 11 +- test/unit/sql/json.test.js | 11 +- test/unit/sql/offset-limit.test.js | 6 + test/unit/sql/order.test.js | 2 + test/unit/sql/remove-column.test.js | 1 + test/unit/sql/select.test.js | 67 +- test/unit/sql/show-constraints.test.js | 2 + test/unit/sql/update.test.js | 3 + test/unit/sql/where.test.js | 34 +- test/unit/transaction.test.js | 7 + yarn.lock | 5 + 77 files changed, 3817 insertions(+), 170 deletions(-) create mode 100644 dev/oracle/latest/docker-compose.yml create mode 100644 dev/oracle/latest/privileges.sql create mode 100755 dev/oracle/latest/start.sh create mode 100755 dev/oracle/latest/stop.sh create mode 100755 dev/oracle/latest/wait-until-healthy.sh create mode 100644 src/dialects/oracle/connection-manager.js create mode 100644 src/dialects/oracle/data-types.js create mode 100644 src/dialects/oracle/index.js create mode 100644 src/dialects/oracle/query-generator.js create mode 100644 src/dialects/oracle/query-interface.js create mode 100644 src/dialects/oracle/query.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89e6c4d486d5..0011f3ee6b1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,33 @@ jobs: - run: yarn install --frozen-lockfile - run: yarn add --dev typescript@~${{ matrix.ts-version }} - run: yarn test-typings + test-oracle: + strategy: + fail-fast: false + matrix: + node-version: [10, 16] + name: Oracle DB (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + env: + DIALECT: oracle + SEQ_ORACLE_USER: sequelizetest + SEQ_ORACLE_PW: sequelizepassword + SEQ_ORACLE_DB: XEPDB1 + SEQ_ORACLE_HOST: localhost + SEQ_ORACLE_PORT: 1521 + UV_THREADPOOL_SIZE: 128 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: yarn install --frozen-lockfile --ignore-engines + - name: Install Local Oracle DB + run: yarn start-oracle + - name: Unit Tests + run: yarn test-unit + - name: Integration Tests + run: yarn test-integration-oracle test-db2: strategy: fail-fast: false diff --git a/dev/oracle/latest/docker-compose.yml b/dev/oracle/latest/docker-compose.yml new file mode 100644 index 000000000000..8c01f794c163 --- /dev/null +++ b/dev/oracle/latest/docker-compose.yml @@ -0,0 +1,17 @@ +# Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved + +services: + oraclexedb: + container_name: oraclexedb + image: gvenzl/oracle-xe:latest + environment: + ORACLE_PASSWORD: password + ports: + - 1521:1521 + healthcheck: + test: ["CMD-SHELL", "sqlplus", "system/password@XEPDB1"] + retries: 10 + +networks: + default: + name: sequelize-oraclexedb-network diff --git a/dev/oracle/latest/privileges.sql b/dev/oracle/latest/privileges.sql new file mode 100644 index 000000000000..afea910fdc49 --- /dev/null +++ b/dev/oracle/latest/privileges.sql @@ -0,0 +1,27 @@ +-- Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved + +create user sequelizetest identified by sequelizepassword; +grant connect to sequelizetest with admin option; +grant create session to sequelizetest with admin option; +grant grant any privilege to sequelizetest with admin option; +grant grant any role to sequelizetest with admin option; +grant create any table to sequelizetest with admin option; +grant insert any table to sequelizetest with admin option; +grant select any table to sequelizetest with admin option; +grant update any table to sequelizetest with admin option; +grant delete any table to sequelizetest with admin option; +grant drop any table to sequelizetest with admin option; +grant create view to sequelizetest with admin option; +grant create user to sequelizetest with admin option; +grant drop user to sequelizetest with admin option; +grant create any trigger to sequelizetest with admin option; +grant create any procedure to sequelizetest with admin option; +grant create any sequence to sequelizetest with admin option; +grant select any sequence to sequelizetest with admin option; +grant drop any sequence to sequelizetest with admin option; +grant create any synonym to sequelizetest with admin option; +grant create any index to sequelizetest with admin option; +grant alter user to sequelizetest with admin option; +grant alter any table to sequelizetest with admin option; +alter user sequelizetest quota unlimited on users; +exit; diff --git a/dev/oracle/latest/start.sh b/dev/oracle/latest/start.sh new file mode 100755 index 000000000000..d407f8567a8f --- /dev/null +++ b/dev/oracle/latest/start.sh @@ -0,0 +1,44 @@ +# Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved + +#!/usr/bin/env bash +set -Eeuxo pipefail # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" # https://stackoverflow.com/a/17744637 + +# Remove an existing Oracle DB docker image +docker-compose -p oraclexedb down --remove-orphans + +# Bring up new Oracle DB docker image +docker-compose -p oraclexedb up -d + +# Wait until Oracle DB is set up and docker state is healthy +./wait-until-healthy.sh oraclexedb + +# Moving privileges.sql to docker container +docker cp privileges.sql oraclexedb:/opt/oracle/. + +# Granting all the needed privileges to sequelizetest user +docker exec -t oraclexedb sqlplus system/password@XEPDB1 @privileges.sql + +# Setting up Oracle instant client for oracledb +if [ ! -d ~/oracle ] && [ $(uname) == 'Linux' ] +then + mkdir ~/oracle && + wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basic-linuxx64.zip --no-check-certificate && + unzip instantclient-basic-linuxx64.zip -d ~/oracle/ && + rm instantclient-basic-linuxx64.zip && + mv ~/oracle/instantclient_21_6 ~/oracle/instantclient + + echo "Local Oracle instant client has been setup!" +elif [ ! -d ~/Downloads/instantclient_19_8 ] && [ $(uname) == 'Darwin' ] +then + curl -O https://download.oracle.com/otn_software/mac/instantclient/instantclient-basic-macos.dmg && + hdiutil mount instantclient-basic-macos.dmg && + /Volumes/instantclient-basic-macos.x64-19.8.0.0.0dbru/install_ic.sh && + hdiutil unmount /Volumes/instantclient-basic-macos.x64-19.8.0.0.0dbru && + rm instantclient-basic-macos.dmg && + ln -s ~/Downloads/instantclient_19_8/libclntsh.dylib ../../../node_modules/oracledb/build/Release/ + + echo "Local Oracle instant client has been setup!" +fi + +echo "Local Oracle DB is ready for use!" diff --git a/dev/oracle/latest/stop.sh b/dev/oracle/latest/stop.sh new file mode 100755 index 000000000000..694516a698ac --- /dev/null +++ b/dev/oracle/latest/stop.sh @@ -0,0 +1,10 @@ +# Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved + +#!/usr/bin/env bash +set -Eeuxo pipefail # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" # https://stackoverflow.com/a/17744637 + + +docker-compose -p oraclexedb down --remove-orphans + +echo "Local Oracle DB instance stopped (if it was running)." diff --git a/dev/oracle/latest/wait-until-healthy.sh b/dev/oracle/latest/wait-until-healthy.sh new file mode 100755 index 000000000000..53b85e55a37d --- /dev/null +++ b/dev/oracle/latest/wait-until-healthy.sh @@ -0,0 +1,23 @@ +# Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved + +#!/usr/bin/env bash + +if [ "$#" -ne 1 ]; then + >&2 echo "Please provide the container name or hash" + exit 1 +fi + +for _ in {1..50} +do + state=$(docker inspect -f '{{ .State.Health.Status }}' $1 2>&1) + return_code=$? + if [ ${return_code} -eq 0 ] && [ "$state" == "healthy" ]; then + echo "$1 is healthy!" + sleep 60 + exit 0 + fi + sleep 6 +done + +>&2 echo "Timeout of 5m exceeded when waiting for container to be healthy: $1" +exit 1 diff --git a/package.json b/package.json index 2d37e64aec2e..0f59e1320891 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "mysql2": "^2.3.3", "node-hook": "^1.0.0", "nyc": "^15.1.0", + "oracledb": "^5.4.0", "p-map": "^4.0.0", "p-props": "^4.0.0", "p-settle": "^4.1.1", @@ -153,6 +154,7 @@ "db2", "ibm_db", "sql", + "oracledb", "sqlserver", "snowflake", "orm", @@ -232,17 +234,20 @@ "start-postgres": "bash dev/postgres/10/start.sh", "start-mssql": "bash dev/mssql/2019/start.sh", "start-db2": "bash dev/db2/11.5/start.sh", + "start-oracle": "bash dev/oracle/latest/start.sh", "stop-mariadb": "bash dev/mariadb/10.3/stop.sh", "stop-mysql": "bash dev/mysql/5.7/stop.sh", "stop-mysql-8": "bash dev/mysql/8.0/stop.sh", "stop-postgres": "bash dev/postgres/10/stop.sh", "stop-mssql": "bash dev/mssql/2019/stop.sh", "stop-db2": "bash dev/db2/11.5/stop.sh", + "stop-oracle": "bash dev/oracle/latest/stop.sh", "restart-mariadb": "npm run start-mariadb", "restart-mysql": "npm run start-mysql", "restart-postgres": "npm run start-postgres", "restart-mssql": "npm run start-mssql", "restart-db2": "npm run start-db2", + "restart-oracle": "npm run start-oracle", "----------------------------------------- local tests ---------------------------------------------": "", "test-unit-mariadb": "cross-env DIALECT=mariadb npm run test-unit", "test-unit-mysql": "cross-env DIALECT=mysql npm run test-unit", @@ -252,7 +257,8 @@ "test-unit-mssql": "cross-env DIALECT=mssql npm run test-unit", "test-unit-db2": "cross-env DIALECT=db2 npm run test-unit", "test-unit-snowflake": "cross-env DIALECT=snowflake npm run test-unit", - "test-unit-all": "npm run test-unit-mariadb && npm run test-unit-mysql && npm run test-unit-postgres && npm run test-unit-postgres-native && npm run test-unit-mssql && npm run test-unit-sqlite && npm run test-unit-snowflake && npm run test-unit-db2", + "test-unit-all": "npm run test-unit-mariadb && npm run test-unit-mysql && npm run test-unit-postgres && npm run test-unit-postgres-native && npm run test-unit-mssql && npm run test-unit-sqlite && npm run test-unit-snowflake && npm run test-unit-db2 && npm run test-unit-oracle", + "test-unit-oracle": "cross-env DIALECT=oracle npm run test-unit", "test-integration-mariadb": "cross-env DIALECT=mariadb npm run test-integration", "test-integration-mysql": "cross-env DIALECT=mysql npm run test-integration", "test-integration-postgres": "cross-env DIALECT=postgres npm run test-integration", @@ -261,6 +267,7 @@ "test-integration-mssql": "cross-env DIALECT=mssql npm run test-integration", "test-integration-db2": "cross-env DIALECT=db2 npm run test-integration", "test-integration-snowflake": "cross-env DIALECT=snowflake npm run test-integration", + "test-integration-oracle": "cross-env LD_LIBRARY_PATH=$HOME/oracle/instantclient/ DIALECT=oracle UV_THREADPOOL_SIZE=128 npm run test-integration", "test-mariadb": "cross-env DIALECT=mariadb npm test", "test-mysql": "cross-env DIALECT=mysql npm test", "test-sqlite": "cross-env DIALECT=sqlite npm test", @@ -268,6 +275,7 @@ "test-postgres-native": "cross-env DIALECT=postgres-native npm test", "test-mssql": "cross-env DIALECT=mssql npm test", "test-db2": "cross-env DIALECT=db2 npm test", + "test-oracle": "cross-env LD_LIBRARY_PATH=$HOME/oracle/instantclient/ DIALECT=oracle UV_THREADPOOL_SIZE=128 npm test", "----------------------------------------- development ---------------------------------------------": "", "sscce": "node sscce.js", "sscce-mariadb": "cross-env DIALECT=mariadb node sscce.js", @@ -277,6 +285,7 @@ "sscce-sqlite": "cross-env DIALECT=sqlite node sscce.js", "sscce-mssql": "cross-env DIALECT=mssql node sscce.js", "sscce-db2": "cross-env DIALECT=db2 node sscce.js", + "sscce-oracle": "cross-env LD_LIBRARY_PATH=$HOME/oracle/instantclient/ DIALECT=oracle node sscce.js", "prepare": "npm run build && husky install", "build": "node ./build.js", "---------------------------------------------------------------------------------------------------": "" diff --git a/src/data-types.js b/src/data-types.js index 371652be60f9..efe534ebf0cd 100644 --- a/src/data-types.js +++ b/src/data-types.js @@ -1060,6 +1060,7 @@ dialectMap.sqlite = require('./dialects/sqlite/data-types')(DataTypes); dialectMap.mssql = require('./dialects/mssql/data-types')(DataTypes); dialectMap.db2 = require('./dialects/db2/data-types')(DataTypes); dialectMap.snowflake = require('./dialects/snowflake/data-types')(DataTypes); +dialectMap.oracle = require('./dialects/oracle/data-types')(DataTypes); const dialectList = Object.values(dialectMap); diff --git a/src/dialects/abstract/query-generator.js b/src/dialects/abstract/query-generator.js index b962f3ec7aa5..eec3cfe368d7 100644 --- a/src/dialects/abstract/query-generator.js +++ b/src/dialects/abstract/query-generator.js @@ -88,6 +88,16 @@ class QueryGenerator { return `ALTER TABLE ${this.quoteTable(before)} RENAME TO ${this.quoteTable(after)};`; } + /** + * Helper method for getting the returning into bind information + * that is needed by some dialects (currently Oracle) + * + * @private + */ + getInsertQueryReturnIntoBinds() { + // noop by default + } + /** * Returns an insert into command * @@ -103,12 +113,14 @@ class QueryGenerator { _.defaults(options, this.options); const modelAttributeMap = {}; - const bind = []; + const bind = options.bind || []; const fields = []; const returningModelAttributes = []; + const returnTypes = []; const values = []; const quotedTable = this.quoteTable(table); const bindParam = options.bindParam === undefined ? this.bindParam(bind) : options.bindParam; + const returnAttributes = []; let query; let valueQuery = ''; let emptyQuery = ''; @@ -132,10 +144,14 @@ class QueryGenerator { emptyQuery += ' VALUES ()'; } - if (this._dialect.supports.returnValues && options.returning) { + if ((this._dialect.supports.returnValues || this._dialect.supports.returnIntoValues) && options.returning) { const returnValues = this.generateReturnValues(modelAttributes, options); returningModelAttributes.push(...returnValues.returnFields); + // Storing the returnTypes for dialects that need to have returning into bind information for outbinds + if (this._dialect.supports.returnIntoValues) { + returnTypes.push(...returnValues.returnTypes); + } returningFragment = returnValues.returningFragment; tmpTable = returnValues.tmpTable || ''; outputFragment = returnValues.outputFragment || ''; @@ -244,7 +260,12 @@ class QueryGenerator { emptyQuery += returningFragment; } - query = `${replacements.attributes.length ? valueQuery : emptyQuery};`; + if (this._dialect.supports.returnIntoValues && options.returning) { + // Populating the returnAttributes array and performing operations needed for output binds of insertQuery + this.getInsertQueryReturnIntoBinds(returnAttributes, bind.length, returningModelAttributes, returnTypes, options); + } + + query = `${replacements.attributes.length ? valueQuery : emptyQuery}${returnAttributes.join(',')};`; if (this._dialect.supports.finalTable) { query = `SELECT * FROM FINAL TABLE(${ replacements.attributes.length ? valueQuery : emptyQuery });`; } @@ -383,8 +404,18 @@ class QueryGenerator { const bindParam = options.bindParam === undefined ? this.bindParam(bind) : options.bindParam; if (this._dialect.supports['LIMIT ON UPDATE'] && options.limit) { - if (this.dialect !== 'mssql' && this.dialect !== 'db2') { + if (this.dialect !== 'mssql' && this.dialect !== 'db2' && this.dialect !== 'oracle') { suffix = ` LIMIT ${this.escape(options.limit)} `; + } else if (this.dialect === 'oracle') { + //This cannot be setted in where because rownum will be quoted + if (where && (where.length && where.length > 0 || Object.keys(where).length > 0)) { + //If we have a where clause, we add AND + suffix += ' AND '; + } else { + //No where clause, we add where + suffix += ' WHERE '; + } + suffix += `rownum <= ${this.escape(options.limit)} `; } } @@ -1188,6 +1219,7 @@ class QueryGenerator { let mainJoinQueries = []; let subJoinQueries = []; let query; + const hasAs = this._dialect.name === 'oracle' ? '' : 'AS '; // Aliases can be passed through subqueries and we don't want to reset them if (this.options.minifyAliases && !options.aliasesMapping) { @@ -1312,7 +1344,12 @@ class QueryGenerator { } else { // Ordering is handled by the subqueries, so ordering the UNION'ed result is not needed groupedLimitOrder = options.order; - delete options.order; + + // For the Oracle dialect, the result of a select is a set, not a sequence, and so is the result of UNION. + // So the top level ORDER BY is required + if (this._dialect.name !== 'oracle') { + delete options.order; + } where[Op.placeholder] = true; } @@ -1332,7 +1369,7 @@ class QueryGenerator { model }, model - ).replace(/;$/, '')}) AS sub`; // Every derived table must have its own alias + ).replace(/;$/, '')}) ${hasAs}sub`; // Every derived table must have its own alias const placeHolder = this.whereItemQuery(Op.placeholder, true, { model }); const splicePos = baseQuery.indexOf(placeHolder); @@ -1426,7 +1463,7 @@ class QueryGenerator { if (subQuery) { this._throwOnEmptyAttributes(attributes.main, { modelName: model && model.name, as: mainTable.as }); - query = `SELECT ${attributes.main.join(', ')} FROM (${subQueryItems.join('')}) AS ${mainTable.as}${mainJoinQueries.join('')}${mainQueryItems.join('')}`; + query = `SELECT ${attributes.main.join(', ')} FROM (${subQueryItems.join('')}) ${hasAs}${mainTable.as}${mainJoinQueries.join('')}${mainQueryItems.join('')}`; } else { query = mainQueryItems.join(''); } @@ -1566,6 +1603,8 @@ class QueryGenerator { prefix = `(${this.quoteIdentifier(includeAs.internalAs)}.${attr.replace(/\(|\)/g, '')})`; } else if (/json_extract\(/.test(attr)) { prefix = attr.replace(/json_extract\(/i, `json_extract(${this.quoteIdentifier(includeAs.internalAs)}.`); + } else if (/json_value\(/.test(attr)) { + prefix = attr.replace(/json_value\(/i, `json_value(${this.quoteIdentifier(includeAs.internalAs)}.`); } else { prefix = `${this.quoteIdentifier(includeAs.internalAs)}.${this.quoteIdentifier(attr)}`; } @@ -1822,6 +1861,8 @@ class QueryGenerator { if (this._dialect.supports.returnValues.returning) { returningFragment = ` RETURNING ${returnFields.join(',')}`; + } else if (this._dialect.supports.returnIntoValues) { + returningFragment = ` RETURNING ${returnFields.join(',')} INTO `; } else if (this._dialect.supports.returnValues.output) { outputFragment = ` OUTPUT ${returnFields.map(field => `INSERTED.${field}`).join(',')}`; @@ -1835,7 +1876,7 @@ class QueryGenerator { } } - return { outputFragment, returnFields, returningFragment, tmpTable }; + return { outputFragment, returnFields, returnTypes, returningFragment, tmpTable }; } generateThroughJoin(include, includeAs, parentTableName, topLevelInfo) { diff --git a/src/dialects/oracle/connection-manager.js b/src/dialects/oracle/connection-manager.js new file mode 100644 index 000000000000..c8359669baac --- /dev/null +++ b/src/dialects/oracle/connection-manager.js @@ -0,0 +1,192 @@ +// Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved + +'use strict'; + +const AbstractConnectionManager = require('../abstract/connection-manager'); +const SequelizeErrors = require('../../errors'); +const parserStore = require('../parserStore')('oracle'); +const { logger } = require('../../utils/logger'); +const debug = logger.debugContext('connection:oracle'); +const DataTypes = require('../../data-types').oracle; +const { promisify } = require('util'); +/** + * Oracle Connection Manager + * + * Get connections, validate and disconnect them. + * AbstractConnectionManager pooling use it to handle Oracle specific connections + * Use github.com/oracle/node-oracledb to connect with Oracle server + * + * @private + */ +class ConnectionManager extends AbstractConnectionManager { + constructor(dialect, sequelize) { + super(dialect, sequelize); + + this.sequelize = sequelize; + this.sequelize.config.port = this.sequelize.config.port || 1521; + this.lib = this._loadDialectModule('oracledb'); + this.refreshTypeParser(DataTypes); + this.lib.maxRows = 1000; + if (sequelize.config && 'dialectOptions' in sequelize.config) { + const dialectOptions = sequelize.config.dialectOptions; + if (dialectOptions && 'maxRows' in dialectOptions) { + this.lib.maxRows = sequelize.config.dialectOptions.maxRows; + } + if (dialectOptions && 'fetchAsString' in dialectOptions) { + this.lib.fetchAsString = sequelize.config.dialectOptions.fetchAsString; + } else { + this.lib.fetchAsString = [this.lib.CLOB]; + } + } + // Retrieve BLOB always as Buffer. + this.lib.fetchAsBuffer = [this.lib.BLOB]; + } + + /** + * Method for checking the config object passed and generate the full database if not fully passed + * With dbName, host and port, it generates a string like this : 'host:port/dbname' + * + * @param {object} config + * @returns {Promise} + * @private + */ + checkConfigObject(config) { + // A connectString should be defined + if (config.database.length === 0) { + let errorToThrow = + 'The database cannot be blank, you must specify the database name (which correspond to the service name'; + errorToThrow += '\n from tnsnames.ora : (HOST = mymachine.example.com)(PORT = 1521)(SERVICE_NAME = orcl)'; + throw new Error(errorToThrow); + } + + if (!config.host || config.host.length === 0) { + throw new Error('You have to specify the host'); + } + + // The connectString has a special format, we check it + // ConnectString format is : host:[port]/service_name + if (config.database.indexOf('/') === -1) { + let connectString = config.host; + + if (config.port && config.port !== 0) { + connectString += `:${config.port}`; + } else { + connectString += ':1521'; //Default port number + } + connectString += `/${config.database}`; + config.database = connectString; + } + } + + // Expose this as a method so that the parsing may be updated when the user has added additional, custom types + _refreshTypeParser(dataType) { + parserStore.refresh(dataType); + } + + _clearTypeParser() { + parserStore.clear(); + } + + /** + * Connect with Oracle database based on config, Handle any errors in connection + * Set the pool handlers on connection.error + * Also set proper timezone once connection is connected. + * + * @param {object} config + * @returns {Promise} + * @private + */ + async connect(config) { + const connectionConfig = { + user: config.username, + host: config.host, + port: config.port, + database: config.database, + password: config.password, + externalAuth: config.externalAuth, + stmtCacheSize: 0, + connectString: config.database, + ...config.dialectOptions + }; + + try { + // Check the config object + this.checkConfigObject(connectionConfig); + + // We assume that the database has been correctly formed + connectionConfig.connectString = connectionConfig.database; + + // We check if there are dialect options + if (config.dialectOptions) { + // const dialectOptions = config.dialectOptions; + + // //If stmtCacheSize is defined, we set it + // if (dialectOptions && 'stmtCacheSize' in dialectOptions) { + // connectionConfig.stmtCacheSize = dialectOptions.stmtCacheSize; + // } + + Object.keys(config.dialectOptions).forEach(key => { + connectionConfig[key] = config.dialectOptions[key]; + }); + } + + const connection = await this.lib.getConnection(connectionConfig); + + debug('connection acquired'); + connection.on('error', error => { + switch (error.code) { + case 'ESOCKET': + case 'ECONNRESET': + case 'EPIPE': + case 'PROTOCOL_CONNECTION_LOST': + this.pool.destroy(connection); + } + }); + + return connection; + } catch (err) { + // We split to get the error number; it comes as ORA-XXXXX: + let errorCode = err.message.split(':'); + errorCode = errorCode[0]; + + switch (errorCode) { + case 'ORA-28000': // Account locked + case 'ORA-12541': // ORA-12541: TNS:No listener + throw new SequelizeErrors.ConnectionRefusedError(err); + case 'ORA-01017': // ORA-01017 : invalid username/password; logon denied + throw new SequelizeErrors.AccessDeniedError(err); + case 'ORA-12154': + throw new SequelizeErrors.HostNotReachableError(err); //ORA-12154: TNS:could not resolve the connect identifier specified + case 'ORA-12514': // ORA-12514: TNS:listener does not currently know of service requested in connect descriptor + throw new SequelizeErrors.HostNotFoundError(err); + // case 'ORA-12541': // ORA-12541: TNS:No listener + // throw new SequelizeErrors.AccessDeniedError(err); + default: + throw new SequelizeErrors.ConnectionError(err); + } + } + } + + async disconnect(connection) { + if (!connection.isHealthy()) { + debug('connection tried to disconnect but was already at CLOSED state'); + return; + } + + return await promisify(callback => connection.close(callback))(); + } + + /** + * Checking if the connection object is valid and the connection is healthy + * + * @param {object} connection + * @private + */ + validate(connection) { + return connection && connection.isHealthy(); + } +} + +module.exports = ConnectionManager; +module.exports.ConnectionManager = ConnectionManager; +module.exports.default = ConnectionManager; diff --git a/src/dialects/oracle/data-types.js b/src/dialects/oracle/data-types.js new file mode 100644 index 000000000000..d00024881b78 --- /dev/null +++ b/src/dialects/oracle/data-types.js @@ -0,0 +1,470 @@ +// Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved + +'use strict'; + +const moment = require('moment'); +const momentTz = require('moment-timezone'); + +module.exports = BaseTypes => { + const warn = BaseTypes.ABSTRACT.warn.bind( + undefined, + 'https://docs.oracle.com/database/122/SQLRF/Data-Types.htm#SQLRF30020' + ); + + BaseTypes.DATE.types.oracle = ['TIMESTAMP', 'TIMESTAMP WITH LOCAL TIME ZONE']; + BaseTypes.STRING.types.oracle = ['VARCHAR2', 'NVARCHAR2']; + BaseTypes.CHAR.types.oracle = ['CHAR', 'RAW']; + BaseTypes.TEXT.types.oracle = ['CLOB']; + BaseTypes.TINYINT.types.oracle = ['NUMBER']; + BaseTypes.SMALLINT.types.oracle = ['NUMBER']; + BaseTypes.MEDIUMINT.types.oracle = ['NUMBER']; + BaseTypes.INTEGER.types.oracle = ['INTEGER']; + BaseTypes.BIGINT.types.oracle = ['NUMBER']; + BaseTypes.FLOAT.types.oracle = ['BINARY_FLOAT']; + BaseTypes.DATEONLY.types.oracle = ['DATE']; + BaseTypes.BOOLEAN.types.oracle = ['CHAR(1)']; + BaseTypes.BLOB.types.oracle = ['BLOB']; + BaseTypes.DECIMAL.types.oracle = ['NUMBER']; + BaseTypes.UUID.types.oracle = ['VARCHAR2']; + BaseTypes.ENUM.types.oracle = ['VARCHAR2']; + BaseTypes.REAL.types.oracle = ['BINARY_DOUBLE']; + BaseTypes.DOUBLE.types.oracle = ['BINARY_DOUBLE']; + BaseTypes.JSON.types.oracle = ['BLOB']; + BaseTypes.GEOMETRY.types.oracle = false; + + class STRING extends BaseTypes.STRING { + toSql() { + if (this.length > 4000 || this._binary && this._length > 2000) { + warn( + 'Oracle 12 supports length up to 32764; be sure that your administrator has extended the MAX_STRING_SIZE parameter. Check https://docs.oracle.com/database/121/REFRN/GUID-D424D23B-0933-425F-BC69-9C0E6724693C.htm#REFRN10321' + ); + } + if (!this._binary) { + return `NVARCHAR2(${this._length})`; + } + return `RAW(${this._length})`; + } + + _stringify(value, options) { + if (this._binary) { + // For Binary numbers we're converting a buffer to hex then + // sending it over the wire as a string, + // We pass it through escape function to remove un-necessary quotes + // this.format in insert/bulkinsert query calls stringify hence we need to convert binary buffer + // to hex string. Since this block is used by both bind (insert/bulkinsert) and + // non-bind (select query where clause) hence we need to + // have an operation that supports both + return options.escape(value.toString('hex')); + } + return options.escape(value); + } + + _getBindDef(oracledb) { + if (this._binary) { + return { type: oracledb.DB_TYPE_RAW, maxSize: this._length }; + } + return { type: oracledb.DB_TYPE_VARCHAR, maxSize: this._length }; + } + + _bindParam(value, options) { + return options.bindParam(value); + } + } + + STRING.prototype.escape = false; + + class BOOLEAN extends BaseTypes.BOOLEAN { + toSql() { + return 'CHAR(1)'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_CHAR, maxSize: 1 }; + } + + _stringify(value) { + // If value is true we return '1' + // If value is false we return '0' + // Else we return it as is + // Converting number to char since in bindDef + // the type would be oracledb.DB_TYPE_CHAR + return value === true ? '1' : value === false ? '0' : value; + } + + _sanitize(value) { + if (typeof value === 'string') { + // If value is a string we return true if among '1' and 'true' + // We return false if among '0' and 'false' + // Else return the value as is and let the DB raise error for invalid values + return value === '1' || value === 'true' ? true : value === '0' || value === 'false' ? false : value; + } + return super._sanitize(value); + } + } + + class UUID extends BaseTypes.UUID { + toSql() { + return 'VARCHAR2(36)'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_VARCHAR, maxSize: 36 }; + } + } + + class NOW extends BaseTypes.NOW { + toSql() { + return 'SYSDATE'; + } + + _stringify() { + return 'SYSDATE'; + } + } + + class ENUM extends BaseTypes.ENUM { + toSql() { + return 'VARCHAR2(512)'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_VARCHAR, maxSize: 512 }; + } + } + + class TEXT extends BaseTypes.TEXT { + toSql() { + return 'CLOB'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_CLOB }; + } + } + + class CHAR extends BaseTypes.CHAR { + toSql() { + if (this._binary) { + return `RAW(${this._length})`; + } + return super.toSql(); + } + + _getBindDef(oracledb) { + if (this._binary) { + return { type: oracledb.DB_TYPE_RAW, maxSize: this._length }; + } + return { type: oracledb.DB_TYPE_CHAR, maxSize: this._length }; + } + + _bindParam(value, options) { + return options.bindParam(value); + } + } + + class DATE extends BaseTypes.DATE { + toSql() { + return 'TIMESTAMP WITH LOCAL TIME ZONE'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_TIMESTAMP_LTZ }; + } + + _stringify(date, options) { + const format = 'YYYY-MM-DD HH24:MI:SS.FFTZH:TZM'; + + date = this._applyTimezone(date, options); + + const formatedDate = date.format('YYYY-MM-DD HH:mm:ss.SSS Z'); + + return `TO_TIMESTAMP_TZ('${formatedDate}','${format}')`; + } + + _applyTimezone(date, options) { + if (options.timezone) { + if (momentTz.tz.zone(options.timezone)) { + date = momentTz(date).tz(options.timezone); + } else { + date = moment(date).utcOffset(options.timezone); + } + } else { + date = momentTz(date); + } + return date; + } + + static parse(value, options) { + if (value === null) { + return value; + } + if (options && moment.tz.zone(options.timezone)) { + value = moment.tz(value.toString(), options.timezone).toDate(); + } + return value; + } + + /** + * avoids appending TO_TIMESTAMP_TZ in _stringify + * + * @override + */ + _bindParam(value, options) { + return options.bindParam(value); + } + } + + DATE.prototype.escape = false; + + class DECIMAL extends BaseTypes.DECIMAL { + constructor() { + super(); + this.key = 'DECIMAL'; + } + + toSql() { + let result = ''; + if (this._length) { + result += `(${this._length}`; + if (typeof this._decimals === 'number') { + result += `,${this._decimals}`; + } + result += ')'; + } + + if (!this._length && this._precision) { + result += `(${this._precision}`; + if (typeof this._scale === 'number') { + result += `,${this._scale}`; + } + result += ')'; + } + + return `NUMBER${result}`; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_NUMBER }; + } + } + + class TINYINT extends BaseTypes.TINYINT { + toSql() { + return 'NUMBER(3)'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_NUMBER }; + } + } + + class SMALLINT extends BaseTypes.SMALLINT { + toSql() { + return 'NUMBER(5)'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_NUMBER }; + } + } + + class MEDIUMINT extends BaseTypes.MEDIUMINT { + toSql() { + return 'NUMBER(8)'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_NUMBER }; + } + } + + class BIGINT extends BaseTypes.BIGINT { + constructor(length) { + super(length); + if (!(this instanceof BIGINT)) return new BIGINT(length); + BaseTypes.BIGINT.apply(this, arguments); + + // ORACLE does not support any options for bigint + if (this._length || this.options.length || this._unsigned || this._zerofill) { + this._length = undefined; + this.options.length = undefined; + this._unsigned = undefined; + this._zerofill = undefined; + } + } + + toSql() { + return 'NUMBER(19)'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_NUMBER }; + } + } + + class NUMBER extends BaseTypes.NUMBER { + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_NUMBER }; + } + } + + class INTEGER extends BaseTypes.INTEGER { + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_NUMBER }; + } + } + + class FLOAT extends BaseTypes.FLOAT { + toSql() { + return 'BINARY_FLOAT'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_BINARY_FLOAT }; + } + } + + class REAL extends BaseTypes.REAL { + toSql() { + return 'BINARY_DOUBLE'; + } + + // https://docs.oracle.com/cd/E37502_01/server.751/es_eql/src/ceql_literals_nan.html + _stringify(value) { + if (value === 'Infinity') { + return 'inf'; + } + if (value === '-Infinity') { + return '-inf'; + } + return value; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_BINARY_DOUBLE }; + } + } + + class BLOB extends BaseTypes.BLOB { + // Generic hexify returns X'${hex}' but Oracle expects '${hex}' for BLOB datatype + _hexify(hex) { + return `'${hex}'`; + } + + toSql() { + return 'BLOB'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_BLOB }; + } + } + + class JSONTYPE extends BaseTypes.JSON { + toSql() { + return 'BLOB'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_BLOB }; + } + + _stringify(value, options) { + return options.operation === 'where' && typeof value === 'string' ? value : JSON.stringify(value); + } + + _bindParam(value, options) { + return options.bindParam(Buffer.from(JSON.stringify(value))); + } + } + + class DOUBLE extends BaseTypes.DOUBLE { + constructor(length, decimals) { + super(length, decimals); + if (!(this instanceof DOUBLE)) return new BaseTypes.DOUBLE(length, decimals); + BaseTypes.DOUBLE.apply(this, arguments); + + if (this._length || this._unsigned || this._zerofill) { + this._length = undefined; + this.options.length = undefined; + this._unsigned = undefined; + this._zerofill = undefined; + } + + this.key = 'DOUBLE PRECISION'; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_BINARY_DOUBLE }; + } + + toSql() { + return 'BINARY_DOUBLE'; + } + } + class DATEONLY extends BaseTypes.DATEONLY { + parse(value) { + return moment(value).format('YYYY-MM-DD'); + } + + _sanitize(value) { + if (value) { + return moment(value).format('YYYY-MM-DD'); + } + return value; + } + + _stringify(date) { + // If date is not null only then we format the date + if (date) { + const format = 'YYYY/MM/DD'; + return `TO_DATE('${date}','${format}')`; + } + return date; + } + + _getBindDef(oracledb) { + return { type: oracledb.DB_TYPE_DATE }; + } + + /** + * avoids appending TO_DATE in _stringify + * + * @override + */ + _bindParam(value, options) { + if (typeof value === 'string') { + return options.bindParam(new Date(value)); + } + return options.bindParam(value); + + } + } + + DATEONLY.prototype.escape = false; + + return { + BOOLEAN, + 'DOUBLE PRECISION': DOUBLE, + DOUBLE, + STRING, + TINYINT, + SMALLINT, + MEDIUMINT, + BIGINT, + NUMBER, + INTEGER, + FLOAT, + UUID, + DATEONLY, + DATE, + NOW, + BLOB, + ENUM, + TEXT, + CHAR, + JSON: JSONTYPE, + REAL, + DECIMAL + }; +}; diff --git a/src/dialects/oracle/index.js b/src/dialects/oracle/index.js new file mode 100644 index 000000000000..8222eaf63cbc --- /dev/null +++ b/src/dialects/oracle/index.js @@ -0,0 +1,66 @@ +// Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved + +'use strict'; + +const _ = require('lodash'); +const AbstractDialect = require('../abstract'); +const ConnectionManager = require('./connection-manager'); +const Query = require('./query'); +const QueryGenerator = require('./query-generator'); +const DataTypes = require('../../data-types').oracle; +const { OracleQueryInterface } = require('./query-interface'); + +class OracleDialect extends AbstractDialect { + constructor(sequelize) { + super(); + this.sequelize = sequelize; + this.connectionManager = new ConnectionManager(this, sequelize); + this.connectionManager.initPools(); + this.queryGenerator = new QueryGenerator({ + _dialect: this, + sequelize + }); + this.queryInterface = new OracleQueryInterface(sequelize, this.queryGenerator); + } +} + +OracleDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.supports), { + 'VALUES ()': true, + 'LIMIT ON UPDATE': true, + IGNORE: ' IGNORE', + lock: false, + forShare: ' IN SHARE MODE', + index: { + collate: false, + length: false, + parser: false, + type: false, + using: false + }, + constraints: { + restrict: false + }, + returnValues: false, + returnIntoValues: true, + 'ORDER NULLS': true, + ignoreDuplicates: ' IGNORE', + schemas: true, + updateOnDuplicate: false, + indexViaAlter: false, + NUMERIC: true, + JSON: true, + upserts: true, + bulkDefault: true, + GEOMETRY: false +}); + +OracleDialect.prototype.defaultVersion = '18.4.0'; +OracleDialect.prototype.Query = Query; +OracleDialect.prototype.queryGenerator = QueryGenerator; +OracleDialect.prototype.DataTypes = DataTypes; +OracleDialect.prototype.name = 'oracle'; +OracleDialect.prototype.TICK_CHAR = '"'; +OracleDialect.prototype.TICK_CHAR_LEFT = OracleDialect.prototype.TICK_CHAR; +OracleDialect.prototype.TICK_CHAR_RIGHT = OracleDialect.prototype.TICK_CHAR; + +module.exports = OracleDialect; diff --git a/src/dialects/oracle/query-generator.js b/src/dialects/oracle/query-generator.js new file mode 100644 index 000000000000..dc4583e370d6 --- /dev/null +++ b/src/dialects/oracle/query-generator.js @@ -0,0 +1,1443 @@ +// Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved + +'use strict'; + +/* jshint -W110 */ +const Utils = require('../../utils'); +const DataTypes = require('../../data-types'); +const AbstractQueryGenerator = require('../abstract/query-generator'); +const _ = require('lodash'); +const util = require('util'); +const Transaction = require('../../transaction'); + +/** + * list of reserved words in Oracle DB 21c + * source: https://docs.oracle.com/en/cloud/saas/taleo-enterprise/21d/otccu/r-reservedwords.html#id08ATA0RF05Z + * + * @private + */ +const ORACLE_RESERVED_WORDS = ['ACCESS', 'ADD', 'ALL', 'ALTER', 'AND', 'ANY', 'ARRAYLEN', 'AS', 'ASC', 'AUDIT', 'BETWEEN', 'BY', 'CHAR', 'CHECK', 'CLUSTER', 'COLUMN', 'COMMENT', 'COMPRESS', 'CONNECT', 'CREATE', 'CURRENT', 'DATE', 'DECIMAL', 'DEFAULT', 'DELETE', 'DESC', 'DISTINCT', 'DROP', 'ELSE', 'EXCLUSIVE', 'EXISTS', 'FILE', 'FLOAT', 'FOR', 'FROM', 'GRANT', 'GROUP', 'HAVING', 'IDENTIFIED', 'IMMEDIATE', 'IN', 'INCREMENT', 'INDEX', 'INITIAL', 'INSERT', 'INTEGER', 'INTERSECT', 'INTO', 'IS', 'LEVEL', 'LIKE', 'LOCK', 'LONG', 'MAXEXTENTS', 'MINUS', 'MODE', 'MODIFY', 'NOAUDIT', 'NOCOMPRESS', 'NOT', 'NOTFOUND', 'NOWAIT', 'NULL', 'NUMBER', 'OF', 'OFFLINE', 'ON', 'ONLINE', 'OPTION', 'OR', 'ORDER', 'PCTFREE', 'PRIOR', 'PRIVILEGES', 'PUBLIC', 'RAW', 'RENAME', 'RESOURCE', 'REVOKE', 'ROW', 'ROWID', 'ROWLABEL', 'ROWNUM', 'ROWS', 'SELECT', 'SESSION', 'SET', 'SHARE', 'SIZE', 'SMALLINT', 'SQLBUF', 'START', 'SUCCESSFUL', 'SYNONYM', 'SYSDATE', 'TABLE', 'THEN', 'TO', 'TRIGGER', 'UID', 'UNION', 'UNIQUE', 'UPDATE', 'USER', 'VALIDATE', 'VALUES', 'VARCHAR', 'VARCHAR2', 'VIEW', 'WHENEVER', 'WHERE', 'WITH']; +const JSON_FUNCTION_REGEX = /^\s*((?:[a-z]+_){0,2}jsonb?(?:_[a-z]+){0,2})\([^)]*\)/i; +const JSON_OPERATOR_REGEX = /^\s*(->>?|@>|<@|\?[|&]?|\|{2}|#-)/i; +const TOKEN_CAPTURE_REGEX = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i; + +class OracleQueryGenerator extends AbstractQueryGenerator { + constructor(options) { + super(options); + } + + /** + * Returns the value as it is stored in the Oracle DB + * + * @param {string} value + */ + getCatalogName(value) { + if (value) { + if (this.options.quoteIdentifiers === false) { + const quotedValue = this.quoteIdentifier(value); + if (quotedValue === value) { + value = value.toUpperCase(); + } + } + } + return value; + } + + /** + * Returns the tableName and schemaName as it is stored the Oracle DB + * + * @param {object|string} table + */ + getSchemaNameAndTableName(table) { + const tableName = this.getCatalogName(table.tableName || table); + const schemaName = this.getCatalogName(table.schema); + return [tableName, schemaName]; + } + + createSchema(schema) { + const quotedSchema = this.quoteTable(schema); + const schemaName = this.getCatalogName(schema); + return [ + 'DECLARE', + ' V_COUNT INTEGER;', + ' V_CURSOR_NAME INTEGER;', + ' V_RET INTEGER;', + 'BEGIN', + ' SELECT COUNT(1) INTO V_COUNT FROM ALL_USERS WHERE USERNAME = ', + wrapSingleQuote(schemaName), + ';', + ' IF V_COUNT = 0 THEN', + ' EXECUTE IMMEDIATE ', + wrapSingleQuote(`CREATE USER ${ quotedSchema } IDENTIFIED BY 12345 DEFAULT TABLESPACE USERS`), + ';', + ' EXECUTE IMMEDIATE ', + wrapSingleQuote(`GRANT "CONNECT" TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + wrapSingleQuote(`GRANT create table TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + wrapSingleQuote(`GRANT create view TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + wrapSingleQuote(`GRANT create any trigger TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + wrapSingleQuote(`GRANT create any procedure TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + wrapSingleQuote(`GRANT create sequence TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + wrapSingleQuote(`GRANT create synonym TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + wrapSingleQuote(`ALTER USER ${quotedSchema} QUOTA UNLIMITED ON USERS`), + ';', + ' END IF;', + 'END;' + ].join(' '); + } + + showSchemasQuery() { + return 'SELECT USERNAME AS "schema_name" FROM ALL_USERS WHERE COMMON = (\'NO\') AND USERNAME != user'; + } + + dropSchema(schema) { + const schemaName = this.getCatalogName(schema); + return [ + 'DECLARE', + ' V_COUNT INTEGER;', + 'BEGIN', + ' V_COUNT := 0;', + ' SELECT COUNT(1) INTO V_COUNT FROM ALL_USERS WHERE USERNAME = ', + wrapSingleQuote(schemaName), + ';', + ' IF V_COUNT != 0 THEN', + ' EXECUTE IMMEDIATE ', + wrapSingleQuote(`DROP USER ${this.quoteTable(schema)} CASCADE`), + ';', + ' END IF;', + 'END;' + ].join(' '); + } + + versionQuery() { + return "SELECT VERSION FROM PRODUCT_COMPONENT_VERSION WHERE PRODUCT LIKE 'Oracle%'"; + } + + createTableQuery(tableName, attributes, options) { + const primaryKeys = [], + foreignKeys = {}, + attrStr = [], + self = this, + checkStr = []; + + const values = { + table: this.quoteTable(tableName) + }; + + const chkRegex = /CHECK \(([a-zA-Z_.0-9]*) (.*)\)/g; // Check regex + + // Starting by dealing with all attributes + for (let attr in attributes) { + if (Object.prototype.hasOwnProperty.call(attributes, attr)) { + const dataType = attributes[attr]; + let match; + + attr = this.quoteIdentifier(attr); + + // ORACLE doesn't support inline REFERENCES declarations: move to the end + if (_.includes(dataType, 'PRIMARY KEY')) { + // Primary key + primaryKeys.push(attr); + if (_.includes(dataType, 'REFERENCES')) { + match = dataType.match(/^(.+) (REFERENCES.*)$/); + attrStr.push(`${attr} ${match[1].replace(/PRIMARY KEY/, '')}`); + + // match[2] already has foreignKeys in correct format so we don't need to replace + foreignKeys[attr] = match[2]; + } else { + attrStr.push(`${attr} ${dataType.replace(/PRIMARY KEY/, '').trim()}`); + } + } else if (_.includes(dataType, 'REFERENCES')) { + // Foreign key + match = dataType.match(/^(.+) (REFERENCES.*)$/); + attrStr.push(`${attr} ${match[1]}`); + + // match[2] already has foreignKeys in correct format so we don't need to replace + foreignKeys[attr] = match[2]; + } else if (_.includes(dataType, 'CHECK')) { + // Check constraints go to the end + match = dataType.match(/^(.+) (CHECK.*)$/); + attrStr.push(`${attr} ${match[1]}`); + match[2] = match[2].replace('ATTRIBUTENAME', attr); + const checkCond = match[2].replace(chkRegex, (match, column, condition) => { + return `CHECK (${this.quoteIdentifier(column)} ${condition})`; + }); + + checkStr.push(checkCond); + } else { + attrStr.push(`${attr} ${dataType}`); + } + } + } + + values['attributes'] = attrStr.join(', '); + + const pkString = primaryKeys + .map( + (pk => { + return this.quoteIdentifier(pk); + }).bind(this) + ) + .join(', '); + + if (pkString.length > 0) { + // PrimarykeyName would be of form "PK_table_col" + // Since values.table and pkstring has quoted values we first replace "" with _ + // then we replace [,"\s] with '' + // If primary key name exceeds 128 then we let Oracle DB autogenerate the constraint name + let primaryKeyName = `PK_${values.table}_${pkString}`.replace(/""/g, '_').replace(/[,"\s]/g, ''); + + if (primaryKeyName.length > 128) { + primaryKeyName = `PK_${values.table}`.replace(/""/g, '_').replace(/[,"\s]/g, ''); + } + + if (primaryKeyName.length > 128) { + primaryKeyName = ''; + } + + if (primaryKeyName.length > 0) { + values.attributes += + `,CONSTRAINT ${this.quoteIdentifier(primaryKeyName)} PRIMARY KEY (${pkString})`; + } else { + values.attributes += `,PRIMARY KEY (${pkString})`; + } + + } + + // Dealing with FKs + for (const fkey in foreignKeys) { + if (Object.prototype.hasOwnProperty.call(foreignKeys, fkey)) { + // Oracle default response for FK, doesn't support if defined + if (foreignKeys[fkey].indexOf('ON DELETE NO ACTION') > -1) { + foreignKeys[fkey] = foreignKeys[fkey].replace('ON DELETE NO ACTION', ''); + } + values.attributes += `,FOREIGN KEY (${this.quoteIdentifier(fkey)}) ${foreignKeys[fkey]}`; + } + } + + if (checkStr.length > 0) { + values.attributes += `, ${checkStr.join(', ')}`; + } + + // Specific case for unique indexes with Oracle, we have to set the constraint on the column, if not, no FK will be possible (ORA-02270: no matching unique or primary key for this column-list) + if (options && options.indexes && options.indexes.length > 0) { + const idxToDelete = []; + options.indexes.forEach((index, idx) => { + if ('unique' in index && (index.unique === true || index.unique.length > 0 && index.unique !== false)) { + // If unique index, transform to unique constraint on column + const fields = index.fields.map(field => { + if (typeof field === 'string') { + return field; + } + return field.attribute; + + }); + + // Now we have to be sure that the constraint isn't already declared in uniqueKeys + let canContinue = true; + if (options.uniqueKeys) { + const keys = Object.keys(options.uniqueKeys); + + for (let fieldIdx = 0; fieldIdx < keys.length; fieldIdx++) { + const currUnique = options.uniqueKeys[keys[fieldIdx]]; + + if (currUnique.fields.length === fields.length) { + // lengths are the same, possible same constraint + for (let i = 0; i < currUnique.fields.length; i++) { + const field = currUnique.fields[i]; + + if (_.includes(fields, field)) { + canContinue = false; + } else { + // We have at least one different column, even if we found the same columns previously, we let the constraint be created + canContinue = true; + break; + } + } + } + } + + if (canContinue) { + let indexName = 'name' in index ? index.name : ''; + + if (indexName === '') { + indexName = this._generateUniqueConstraintName(values.table, fields); + } + const constraintToAdd = { + name: indexName, + fields + }; + if (!('uniqueKeys' in options)) { + options.uniqueKeys = {}; + } + + options.uniqueKeys[indexName] = constraintToAdd; + idxToDelete.push(idx); + } else { + // The constraint already exists, we remove it from the list + idxToDelete.push(idx); + } + } + } + }); + idxToDelete.forEach(idx => { + options.indexes.splice(idx, 1); + }); + } + + if (options && !!options.uniqueKeys) { + _.each(options.uniqueKeys, (columns, indexName) => { + let canBeUniq = false; + + // Check if we can create the unique key + primaryKeys.forEach(primaryKey => { + // We can create an unique constraint if it's not on the primary key AND if it doesn't have unique in its definition + // We replace quotes in primary key with '' + // Primary key would be a list with double quotes in it so we remove the double quotes + primaryKey = primaryKey.replace(/"/g, ''); + + // We check if the unique indexes are already a part of primary key or not + // If it is not then we set canbeuniq to true and add a unique constraint to these fields. + // Else we can ignore unique constraint on these + if (!_.includes(columns.fields, primaryKey)) { + canBeUniq = true; + } + }); + + columns.fields.forEach(field => { + let currField = ''; + if (!_.isString(field)) { + currField = field.attribute.replace(/[.,"\s]/g, ''); + } else { + currField = field.replace(/[.,"\s]/g, ''); + } + if (currField in attributes) { + // If canBeUniq is false we need not replace the UNIQUE for the attribute + // So we replace UNIQUE with '' only if there exists a primary key + if (attributes[currField].toUpperCase().indexOf('UNIQUE') > -1 && canBeUniq) { + // We generate the attribute without UNIQUE + const attrToReplace = attributes[currField].replace('UNIQUE', ''); + // We replace in the final string + values.attributes = values.attributes.replace(attributes[currField], attrToReplace); + } + } + }); + + // Oracle cannot have an unique AND a primary key on the same fields, prior to the primary key + if (canBeUniq) { + if (!_.isString(indexName)) { + indexName = this._generateUniqueConstraintName(values.table, columns.fields); + } + + const index = options.uniqueKeys[columns.name]; + delete options.uniqueKeys[columns.name]; + indexName = indexName.replace(/[.,\s]/g, ''); + columns.name = indexName; + options.uniqueKeys[indexName] = index; + + // We cannot auto-generate unique constraint name because sequelize tries to + // Add unique column again when it doesn't find unique constraint name after doing showIndexQuery + // MYSQL doesn't support constraint name > 64 and they face similar issue if size exceed 64 chars + if (indexName.length > 128) { + values.attributes += `,UNIQUE (${columns.fields.map(field => self.quoteIdentifier(field)).join(', ') })`; + } else { + values.attributes += + `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => self.quoteIdentifier(field)).join(', ') })`; + } + } + }); + } + + // we replace single quotes by two quotes in order for the execute statement to work + const query = Utils.joinSQLFragments([ + 'CREATE TABLE', + values.table, + `(${values.attributes})` + ]).replace(/'/g, "''"); + + return Utils.joinSQLFragments([ + 'BEGIN', + 'EXECUTE IMMEDIATE', + `'${query}';`, + 'EXCEPTION WHEN OTHERS THEN', + 'IF SQLCODE != -955 THEN', + 'RAISE;', + 'END IF;', + 'END;' + ]); + } + + tableExistsQuery(table) { + const [tableName, schemaName] = this.getSchemaNameAndTableName(table); + return `SELECT TABLE_NAME FROM ALL_TABLES WHERE TABLE_NAME = ${wrapSingleQuote(tableName)} AND OWNER = ${table.schema ? wrapSingleQuote(schemaName) : 'USER'}`; + } + + /** + * Generates a name for an unique constraint with the pattern : uniqTABLENAMECOLUMNNAMES + * If this indexName is too long for Oracle, it's hashed to have an acceptable length + * + * @param {object|string} table + * @param {Array} columns + */ + _generateUniqueConstraintName(table, columns) { + const indexName = `uniq${table}${columns.join('')}`.replace(/[.,"\s]/g, '').toLowerCase(); + return indexName; + } + + describeTableQuery(tableName, schema) { + const currTableName = this.getCatalogName(tableName.tableName || tableName); + schema = this.getCatalogName(schema); + // name, type, datalength (except number / nvarchar), datalength varchar, datalength number, nullable, default value, primary ? + return [ + 'SELECT atc.COLUMN_NAME, atc.DATA_TYPE, atc.DATA_LENGTH, atc.CHAR_LENGTH, atc.DEFAULT_LENGTH, atc.NULLABLE, ', + "CASE WHEN ucc.CONSTRAINT_NAME LIKE'%PK%' THEN 'PRIMARY' ELSE '' END AS \"PRIMARY\" ", + 'FROM all_tab_columns atc ', + 'LEFT OUTER JOIN all_cons_columns ucc ON(atc.table_name = ucc.table_name AND atc.COLUMN_NAME = ucc.COLUMN_NAME ) ', + schema + ? `WHERE (atc.OWNER = ${wrapSingleQuote(schema)}) ` + : 'WHERE atc.OWNER = USER ', + `AND (atc.TABLE_NAME = ${wrapSingleQuote(currTableName)})`, + 'ORDER BY "PRIMARY", atc.COLUMN_NAME' + ].join(''); + } + + renameTableQuery(before, after) { + return Utils.joinSQLFragments([ + 'ALTER TABLE', + this.quoteTable(before), + 'RENAME TO', + this.quoteTable(after) + ]); + } + + showConstraintsQuery(table) { + const tableName = this.getCatalogName(table.tableName || table); + return `SELECT CONSTRAINT_NAME constraint_name FROM user_cons_columns WHERE table_name = ${wrapSingleQuote(tableName)}`; + } + + showTablesQuery() { + return 'SELECT owner as table_schema, table_name, 0 as lvl FROM all_tables where OWNER IN(SELECT USERNAME AS "schema_name" FROM ALL_USERS WHERE ORACLE_MAINTAINED = \'N\')'; + } + + dropTableQuery(tableName) { + return Utils.joinSQLFragments([ + 'BEGIN ', + 'EXECUTE IMMEDIATE \'DROP TABLE', + this.quoteTable(tableName), + 'CASCADE CONSTRAINTS PURGE\';', + 'EXCEPTION WHEN OTHERS THEN', + ' IF SQLCODE != -942 THEN', + ' RAISE;', + ' END IF;', + 'END;' + ]); + } + + /* + Modifying the indexname so that it is prefixed with the schema name + otherwise Oracle tries to add the index to the USER schema + @overide + */ + addIndexQuery(tableName, attributes, options, rawTablename) { + if (typeof tableName !== 'string' && attributes.name) { + attributes.name = `${tableName.schema}.${attributes.name}`; + } + return super.addIndexQuery(tableName, attributes, options, rawTablename); + } + + addConstraintQuery(tableName, options) { + options = options || {}; + + const constraintSnippet = this.getConstraintSnippet(tableName, options); + + if (typeof tableName === 'string') { + tableName = this.quoteIdentifier(tableName); + } else { + tableName = this.quoteTable(tableName); + } + + return `ALTER TABLE ${tableName} ADD ${constraintSnippet};`; + } + + addColumnQuery(table, key, dataType) { + dataType.field = key; + + const attribute = Utils.joinSQLFragments([ + this.quoteIdentifier(key), + this.attributeToSQL(dataType, { + context: 'addColumn' + }) + .replace('ATTRIBUTENAME', this.quoteIdentifier(key)) + .replace(/'/g, "'") + ]); + + return Utils.joinSQLFragments([ + 'ALTER TABLE', + this.quoteTable(table), + 'ADD', + attribute + ]); + } + + removeColumnQuery(tableName, attributeName) { + return Utils.joinSQLFragments([ + 'ALTER TABLE', + this.quoteTable(tableName), + 'DROP COLUMN', + this.quoteIdentifier(attributeName), + ';' + ]); + } + + /** + * Function to add new foreign key to the attribute + * Block for add and drop foreign key constraint query + * taking the assumption that there is a single column foreign key reference always + * i.e. we always do - FOREIGN KEY (a) reference B(a) during createTable queryGenerator + * so there would be one and only one match for a constraint name for each column + * and every foreign keyed column would have a different constraint name + * Since sequelize doesn't support multiple column foreign key, added complexity to + * add the feature isn't needed + * + * @param {Array} sql Array with sql blocks + * @param {string} definition The operation that needs to be performed on the attribute + * @param {string|object} table The table that needs to be altered + * @param {string} attributeName The name of the attribute which would get altered + */ + _alterForeignKeyConstraint(sql, definition, table, attributeName) { + const [tableName, schemaName] = this.getSchemaNameAndTableName(table); + const attributeNameConstant = wrapSingleQuote(this.getCatalogName(attributeName)); + const schemaNameConstant = table.schema ? wrapSingleQuote(this.getCatalogName(schemaName)) : 'USER'; + const tableNameConstant = wrapSingleQuote(this.getCatalogName(tableName)); + const getConsNameQuery = [ + 'select constraint_name into cons_name', + 'from (', + ' select distinct cc.owner, cc.table_name, cc.constraint_name,', + ' cc.column_name', + ' as cons_columns', + ' from all_cons_columns cc, all_constraints c', + ' where cc.owner = c.owner', + ' and cc.table_name = c.table_name', + ' and cc.constraint_name = c.constraint_name', + ' and c.constraint_type = \'R\'', + ' group by cc.owner, cc.table_name, cc.constraint_name, cc.column_name', + ')', + 'where owner =', + schemaNameConstant, + 'and table_name =', + tableNameConstant, + 'and cons_columns =', + attributeNameConstant, + ';' + ].join(' '); + const secondQuery = Utils.joinSQLFragments([ + `ALTER TABLE ${this.quoteIdentifier(tableName)}`, + 'ADD FOREIGN KEY', + `(${this.quoteIdentifier(attributeName)})`, + definition.replace(/.+?(?=REFERENCES)/, '') + ]); + const fullQuery = [ + 'BEGIN', + getConsNameQuery, + 'EXCEPTION', + 'WHEN NO_DATA_FOUND THEN', + ' CONS_NAME := NULL;', + 'END;', + 'IF CONS_NAME IS NOT NULL THEN', + ` EXECUTE IMMEDIATE 'ALTER TABLE ${this.quoteTable(table)} DROP CONSTRAINT "'||CONS_NAME||'"';`, + 'END IF;', + `EXECUTE IMMEDIATE '${secondQuery}';` + ].join(' '); + sql.push(fullQuery); + } + + /** + * Function to alter table modify + * + * @param {Array} sql Array with sql blocks + * @param {string} definition The operation that needs to be performed on the attribute + * @param {object|string} table The table that needs to be altered + * @param {string} attributeName The name of the attribute which would get altered + */ + _modifyQuery(sql, definition, table, attributeName) { + let query = Utils.joinSQLFragments([ + 'ALTER TABLE', + this.quoteTable(table), + 'MODIFY', + this.quoteIdentifier(attributeName), + definition + ]); + const secondQuery = query.replace('NOT NULL', '').replace('NULL', ''); + query = [ + 'BEGIN', + `EXECUTE IMMEDIATE '${query}';`, + 'EXCEPTION', + 'WHEN OTHERS THEN', + ' IF SQLCODE = -1442 OR SQLCODE = -1451 THEN', + // We execute the statement without the NULL / NOT NULL clause if the first statement failed due to this + ` EXECUTE IMMEDIATE '${secondQuery}';`, + ' ELSE', + ' RAISE;', + ' END IF;', + 'END;' + ].join(' '); + sql.push(query); + } + + changeColumnQuery(table, attributes) { + const sql = [ + 'DECLARE', + 'CONS_NAME VARCHAR2(200);', + 'BEGIN' + ]; + for (const attributeName in attributes) { + let definition = attributes[attributeName]; + if (definition.match(/REFERENCES/)) { + this._alterForeignKeyConstraint(sql, definition, table, attributeName); + } else { + if (definition.indexOf('CHECK') > -1) { + definition = definition.replace(/'/g, "''"); + } + // Building the modify query + this._modifyQuery(sql, definition, table, attributeName); + } + } + sql.push('END;'); + return sql.join(' '); + } + + renameColumnQuery(tableName, attrBefore, attributes) { + const newName = Object.keys(attributes)[0]; + return `ALTER TABLE ${this.quoteTable(tableName)} RENAME COLUMN ${this.quoteIdentifier(attrBefore)} TO ${this.quoteIdentifier(newName)}`; + } + + /** + * Populates the returnAttributes array with outbind bindByPosition values + * and also the outBindAttributes map with bindDef for outbind of InsertQuery + * + * @param {object} returnAttributes + * @param {number} inbindLength + * @param {Array} returningModelAttributes + * @param {Array} returnTypes + * @param {object} options + * + * @private + */ + getInsertQueryReturnIntoBinds(returnAttributes, inbindLength, returningModelAttributes, returnTypes, options) { + const oracledb = this.sequelize.connectionManager.lib; + const outBindAttributes = {}; + const outbind = []; + const outbindParam = this.bindParam(outbind, inbindLength); + returningModelAttributes.forEach((element, index) => { + // generateReturnValues function quotes identifier based on the quoteIdentifier option + // If the identifier starts with a quote we remove it else we use it as is + if (_.startsWith(element, '"')) { + element = element.substring(1, element.length - 1); + } + outBindAttributes[element] = Object.assign(returnTypes[index]._getBindDef(oracledb), { dir: oracledb.BIND_OUT }); + const returnAttribute = `${this.format(undefined, undefined, { context: 'INSERT' }, outbindParam)}`; + returnAttributes.push(returnAttribute); + }); + options.outBindAttributes = outBindAttributes; + } + + /** + * Override of upsertQuery, Oracle specific + * Using PL/SQL for finding the row + * + * @param {object|string} tableName + * @param {Array} insertValues + * @param {Array} updateValues + * @param {Array} where + * @param {object} model + * @param {object} options + */ + upsertQuery(tableName, insertValues, updateValues, where, model, options) { + const rawAttributes = model.rawAttributes; + const updateQuery = this.updateQuery(tableName, updateValues, where, options, rawAttributes); + // This bind is passed so that the insert query starts appending to this same bind array + options.bind = updateQuery.bind; + const insertQuery = this.insertQuery(tableName, insertValues, rawAttributes, options); + + const sql = [ + 'DECLARE ', + 'BEGIN ', + updateQuery.query ? [ + updateQuery.query, + '; ', + ' IF ( SQL%ROWCOUNT = 0 ) THEN ', + insertQuery.query, + ' :isUpdate := 0; ', + 'ELSE ', + ' :isUpdate := 1; ', + ' END IF; ' + ].join('') : [ + insertQuery.query, + ' :isUpdate := 0; ', + // If there is a conflict on insert we ignore + 'EXCEPTION WHEN OTHERS THEN', + ' IF SQLCODE != -1 THEN', + ' RAISE;', + ' END IF;' + ].join(''), + 'END;' + ]; + + const query = sql.join(''); + const result = { query }; + + if (options.bindParam !== false) { + result.bind = updateQuery.bind || insertQuery.bind; + } + + return result; + } + + /** + * Returns an insert into command for multiple values. + * + * @param {string} tableName + * @param {object} fieldValueHashes + * @param {object} options + * @param {object} fieldMappedAttributes + * + * @private + */ + bulkInsertQuery(tableName, fieldValueHashes, options, fieldMappedAttributes) { + options = options || {}; + options.executeMany = true; + fieldMappedAttributes = fieldMappedAttributes || {}; + + const tuples = []; + const allColumns = {}; + const inBindBindDefMap = {}; + const outBindBindDefMap = {}; + const oracledb = this.sequelize.connectionManager.lib; + + // Generating the allColumns map + // The data is provided as an array of objects. + // Each object may contain differing numbers of attributes. + // A set of the attribute names that are used in all objects must be determined. + // The allColumns map contains the column names and indicates whether the value is generated or not + // We set allColumns[key] to true if the field is an + // auto-increment field and the value given is null and fieldMappedAttributes[key] + // is valid for the specific column else it is set to false + for (const fieldValueHash of fieldValueHashes) { + _.forOwn(fieldValueHash, (value, key) => { + allColumns[key] = fieldMappedAttributes[key] && fieldMappedAttributes[key].autoIncrement === true && value === null; + }); + } + + // Building the inbind parameter + // A list that would have inbind positions like [:1, :2, :3...] to be used in generating sql string + let inBindPosition; + // Iterating over each row of the fieldValueHashes + for (const fieldValueHash of fieldValueHashes) { + // Has each column for a row after coverting it to appropriate format using this.format function + // like ['Mick', 'Broadstone', 2022-02-16T05:24:18.949Z, 2022-02-16T05:24:18.949Z], + const tuple = []; + // A function expression for this.bindParam/options.bindparam function + // This function is passed to this.format function which inserts column values to the tuple list + // using _bindParam/_stringify function in data-type.js file + const inbindParam = options.bindParam === undefined ? this.bindParam(tuple) : options.bindParam; + // We are iterating over each col + // and pushing the given values to tuple list using this.format function + // and also simultaneously generating the bindPosition + // tempBindPostions has the inbind positions + const tempBindPositions = Object.keys(allColumns).map(key => { + if (allColumns[key] === true) { + // We had set allAttributes[key] to true since at least one row for an auto increment column was null + // If we get any other row that has this specific column as non-null we must raise an error + // Since for an auto-increment column, either all row has to be null or all row has to be a non-null + if (fieldValueHash[key] !== null) { + throw Error('For an auto-increment column either all row must be null or non-null, a mix of null and non-null is not allowed!'); + } + // Return DEFAULT for auto-increment column and if all values for the column is null in each row + return 'DEFAULT'; + } + // Sanitizes the values given by the user and pushes it to the tuple list using inBindParam function and + // also generates the inbind position for the sql string for example (:1, :2, :3.....) which is a by product of the push + return this.format(fieldValueHash[key], fieldMappedAttributes[key], { context: 'INSERT' }, inbindParam); + }); + + // Even though the bind variable positions are calculated for each row we only retain the values for the first row + // since the values will be identical + if (!inBindPosition) { + inBindPosition = tempBindPositions; + } + // Adding the row to the array of rows that will be supplied to executeMany() + tuples.push(tuple); + } + + // The columns that we are expecting to be returned from the DB like ["id1", "id2"...] + const returnColumn = []; + // The outbind positions for the returning columns like [:3, :4, :5....] + const returnColumnBindPositions = []; + // Has the columns name in which data would be inserted like ["id", "name".....] + const insertColumns = []; + // Iterating over the allColumns keys to get the bindDef for inbind and outbinds + // and also to get the list of insert and return column after applying this.quoteIdentifier + for (const key of Object.keys(allColumns)) { + // If fieldMappenAttributes[attr] is defined we generate the bindDef + // and return clause else we can skip it + if (fieldMappedAttributes[key]) { + // BindDef for the specific column + const bindDef = fieldMappedAttributes[key].type._getBindDef(oracledb); + if (allColumns[key]) { + // Binddef for outbinds + bindDef.dir = oracledb.BIND_OUT; + outBindBindDefMap[key] = bindDef; + + // Building the outbind parameter list + // ReturnColumn has the column name for example "id", "usedId", quoting depends on quoteIdentifier option + returnColumn.push(this.quoteIdentifier(key)); + // Pushing the outbind index to the returnColumnPositions to generate (:3, :4, :5) + // The start offset depend on the tuple length (bind array size of a particular row) + // the outbind position starts after the position where inbind position ends + returnColumnBindPositions.push(`:${tuples[0].length + returnColumn.length}`); + } else { + // Binddef for inbinds + bindDef.dir = oracledb.BIND_IN; + inBindBindDefMap[key] = bindDef; + } + } + // Quoting and pushing each insert column based on quoteIdentifier option + insertColumns.push(this.quoteIdentifier(key)); + } + + // Generating the sql query + let query = Utils.joinSQLFragments([ + 'INSERT', + 'INTO', + // Table name for the table in which data needs to inserted + this.quoteTable(tableName), + // Columns names for the columns of the table (example "a", "b", "c" - quoting depends on the quoteidentifier option) + `(${insertColumns.join(',')})`, + 'VALUES', + // InBind position for the insert query (for example :1, :2, :3....) + `(${inBindPosition})` + ]); + + // If returnColumn.length is > 0 + // then the returning into clause is needed + if (returnColumn.length > 0) { + options.outBindAttributes = outBindBindDefMap; + query = Utils.joinSQLFragments([ + query, + 'RETURNING', + // List of return column (for example "id", "userId"....) + `${returnColumn.join(',')}`, + 'INTO', + // List of outbindPosition (for example :4, :5, :6....) + // Start offset depends on where inbindPosition end + `${returnColumnBindPositions}` + ]); + } + + // Binding the bind variable to result + const result = { query }; + // Binding the bindParam to result + // Tuple has each row for the insert query + result.bind = tuples; + // Setting options.inbindAttribute + options.inbindAttributes = inBindBindDefMap; + return result; + } + + truncateTableQuery(tableName) { + return `TRUNCATE TABLE ${this.quoteTable(tableName)}`; + } + + deleteQuery(tableName, where, options, model) { + options = options || {}; + + const table = tableName; + + where = this.getWhereConditions(where, null, model, options); + let queryTmpl; + // delete with limit and optional condition on Oracle: DELETE FROM WHERE rowid in (SELECT rowid FROM WHERE AND rownum <= ) + // Note that the condition has to be in the subquery; otherwise, the subquery would select arbitrary rows. + if (options.limit) { + const whereTmpl = where ? ` AND ${where}` : ''; + queryTmpl = + `DELETE FROM ${this.quoteTable(table)} WHERE rowid IN (SELECT rowid FROM ${this.quoteTable(table)} WHERE rownum <= ${options.limit}${ + whereTmpl + })`; + } else { + const whereTmpl = where ? ` WHERE ${where}` : ''; + queryTmpl = `DELETE FROM ${this.quoteTable(table)}${whereTmpl}`; + } + return queryTmpl; + } + + showIndexesQuery(table) { + const [tableName, owner] = this.getSchemaNameAndTableName(table); + const sql = [ + 'SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, i.descend ', + 'FROM all_ind_columns i ', + 'INNER JOIN all_indexes u ', + 'ON (u.table_name = i.table_name AND u.index_name = i.index_name) ', + `WHERE i.table_name = ${wrapSingleQuote(tableName)}`, + ' AND u.TABLE_OWNER = ', + owner ? wrapSingleQuote(owner) : 'USER', + ' ORDER BY INDEX_NAME, COLUMN_POSITION' + ]; + + return sql.join(''); + } + + removeIndexQuery(tableName, indexNameOrAttributes) { + let indexName = indexNameOrAttributes; + + if (typeof indexName !== 'string') { + indexName = Utils.underscore(`${tableName }_${indexNameOrAttributes.join('_')}`); + } + + return `DROP INDEX ${this.quoteIdentifier(indexName)}`; + } + + attributeToSQL(attribute, options) { + if (!_.isPlainObject(attribute)) { + attribute = { + type: attribute + }; + } + + // TODO: Address on update cascade issue whether to throw error or ignore. + // Add this to documentation when merging to sequelize-main + // ON UPDATE CASCADE IS NOT SUPPORTED BY ORACLE. + attribute.onUpdate = ''; + + // handle self referential constraints + if (attribute.references) { + if (attribute.Model && attribute.Model.tableName === attribute.references.model) { + this.sequelize.log( + 'Oracle does not support self referencial constraints, ' + + 'we will remove it but we recommend restructuring your query' + ); + attribute.onDelete = ''; + } + } + + let template; + + if (attribute.type instanceof DataTypes.ENUM) { + if (attribute.type.values && !attribute.values) attribute.values = attribute.type.values; + + // enums are a special case + template = attribute.type.toSql(); + template += + ` CHECK (ATTRIBUTENAME IN(${ + _.map(attribute.values, value => { + return this.escape(value); + }).join(', ') + }))`; + return template; + } + if (attribute.type instanceof DataTypes.JSON) { + template = attribute.type.toSql(); + template += ' CHECK (ATTRIBUTENAME IS JSON)'; + return template; + } + if (attribute.type instanceof DataTypes.BOOLEAN) { + template = attribute.type.toSql(); + template += + ' CHECK (ATTRIBUTENAME IN(\'1\', \'0\'))'; + return template; + } + if (attribute.autoIncrement) { + template = ' NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY'; + } else if (attribute.type && attribute.type.key === DataTypes.DOUBLE.key) { + template = attribute.type.toSql(); + } else if (attribute.type) { + // setting it to false because oracle doesn't support unsigned int so put a check to make it behave like unsigned int + let unsignedTemplate = ''; + if (attribute.type._unsigned) { + attribute.type._unsigned = false; + unsignedTemplate += ` check(${this.quoteIdentifier(attribute.field)} >= 0)`; + } + template = attribute.type.toString(); + template += unsignedTemplate; + } else { + template = ''; + } + + + // Blobs/texts cannot have a defaultValue + if ( + attribute.type && + attribute.type !== 'TEXT' && + attribute.type._binary !== true && + Utils.defaultValueSchemable(attribute.defaultValue) + ) { + template += ` DEFAULT ${this.escape(attribute.defaultValue)}`; + } + + if (!attribute.autoIncrement) { + // If autoincrement, not null is setted automatically + if (attribute.allowNull === false) { + template += ' NOT NULL'; + } else if (!attribute.primaryKey && !Utils.defaultValueSchemable(attribute.defaultValue)) { + template += ' NULL'; + } + } + + if (attribute.unique === true && !attribute.primaryKey) { + template += ' UNIQUE'; + } + + if (attribute.primaryKey) { + template += ' PRIMARY KEY'; + } + + if ((!options || !options.withoutForeignKeyConstraints) && attribute.references) { + template += ` REFERENCES ${this.quoteTable(attribute.references.model)}`; + + if (attribute.references.key) { + template += ` (${this.quoteIdentifier(attribute.references.key) })`; + } else { + template += ` (${this.quoteIdentifier('id') })`; + } + + if (attribute.onDelete && attribute.onDelete.toUpperCase() !== 'NO ACTION') { + template += ` ON DELETE ${attribute.onDelete.toUpperCase()}`; + } + } + + return template; + } + attributesToSQL(attributes, options) { + const result = {}; + + for (const key in attributes) { + const attribute = attributes[key]; + const attributeName = attribute.field || key; + result[attributeName] = this.attributeToSQL(attribute, options).replace('ATTRIBUTENAME', this.quoteIdentifier(attributeName)); + } + + return result; + } + + createTrigger() { + throwMethodUndefined('createTrigger'); + } + + dropTrigger() { + throwMethodUndefined('dropTrigger'); + } + + renameTrigger() { + throwMethodUndefined('renameTrigger'); + } + + createFunction() { + throwMethodUndefined('createFunction'); + } + + dropFunction() { + throwMethodUndefined('dropFunction'); + } + + renameFunction() { + throwMethodUndefined('renameFunction'); + } + + getConstraintsOnColumn(table, column) { + const [tableName, schemaName] = this.getSchemaNameAndTableName(table); + column = this.getCatalogName(column); + const sql = [ + 'SELECT CONSTRAINT_NAME FROM user_cons_columns WHERE TABLE_NAME = ', + wrapSingleQuote(tableName), + ' and OWNER = ', + table.schema ? wrapSingleQuote(schemaName) : 'USER', + ' and COLUMN_NAME = ', + wrapSingleQuote(column), + ' AND POSITION IS NOT NULL ORDER BY POSITION' + ].join(''); + + return sql; + } + + getForeignKeysQuery(table) { + // We don't call quoteTable as we don't want the schema in the table name, Oracle seperates it on another field + const [tableName, schemaName] = this.getSchemaNameAndTableName(table); + const sql = [ + 'SELECT DISTINCT a.table_name "tableName", a.constraint_name "constraintName", a.owner "owner", a.column_name "columnName",', + ' b.table_name "referencedTableName", b.column_name "referencedColumnName"', + ' FROM all_cons_columns a', + ' JOIN all_constraints c ON a.owner = c.owner AND a.constraint_name = c.constraint_name', + ' join all_cons_columns b on c.owner = b.owner and c.r_constraint_name = b.constraint_name', + " WHERE c.constraint_type = 'R'", + ' AND a.table_name = ', + wrapSingleQuote(tableName), + ' AND a.owner = ', + table.schema ? wrapSingleQuote(schemaName) : 'USER', + ' order by a.table_name, a.constraint_name' + ].join(''); + + return sql; + } + + quoteTable(param, as) { + let table = ''; + + if (_.isObject(param)) { + if (param.schema) { + table += `${this.quoteIdentifier(param.schema)}.`; + } + table += this.quoteIdentifier(param.tableName); + } else { + table = this.quoteIdentifier(param); + } + + // Oracle don't support as for table aliases + if (as) { + if (as.indexOf('.') > -1 || as.indexOf('_') === 0) { + table += ` ${this.quoteIdentifier(as, true)}`; + } else { + table += ` ${this.quoteIdentifier(as)}`; + } + } + return table; + } + + nameIndexes(indexes, rawTablename) { + let tableName; + if (_.isObject(rawTablename)) { + tableName = `${rawTablename.schema}.${rawTablename.tableName}`; + } else { + tableName = rawTablename; + } + return _.map(indexes, index => { + if (!Object.prototype.hasOwnProperty.call(index, 'name')) { + if (index.unique) { + index.name = this._generateUniqueConstraintName(tableName, index.fields); + } else { + const onlyAttributeNames = index.fields.map(field => + typeof field === 'string' ? field : field.name || field.attribute + ); + index.name = Utils.underscore(`${tableName}_${onlyAttributeNames.join('_')}`); + } + } + return index; + }); + } + + dropForeignKeyQuery(tableName, foreignKey) { + return this.dropConstraintQuery(tableName, foreignKey); + } + + getPrimaryKeyConstraintQuery(table) { + const [tableName, schemaName] = this.getSchemaNameAndTableName(table); + const sql = [ + 'SELECT cols.column_name, atc.identity_column ', + 'FROM all_constraints cons, all_cons_columns cols ', + 'INNER JOIN all_tab_columns atc ON(atc.table_name = cols.table_name AND atc.COLUMN_NAME = cols.COLUMN_NAME )', + 'WHERE cols.table_name = ', + wrapSingleQuote(tableName), + 'AND cols.owner = ', + table.schema ? wrapSingleQuote(schemaName) : 'USER ', + "AND cons.constraint_type = 'P' ", + 'AND cons.constraint_name = cols.constraint_name ', + 'AND cons.owner = cols.owner ', + 'ORDER BY cols.table_name, cols.position' + ].join(''); + + return sql; + } + + /** + * Request to know if the table has a identity primary key, returns the name of the declaration of the identity if true + * + * @param {object|string} table + */ + isIdentityPrimaryKey(table) { + const [tableName, schemaName] = this.getSchemaNameAndTableName(table); + return [ + 'SELECT TABLE_NAME,COLUMN_NAME, COLUMN_NAME,GENERATION_TYPE,IDENTITY_OPTIONS FROM DBA_TAB_IDENTITY_COLS WHERE TABLE_NAME = ', + wrapSingleQuote(tableName), + 'AND OWNER = ', + table.schema ? wrapSingleQuote(schemaName) : 'USER ' + ].join(''); + } + + /** + * Drop identity + * Mandatory, Oracle doesn't support dropping a PK column if it's an identity -> results in database corruption + * + * @param {object|string} tableName + * @param {string} columnName + */ + dropIdentityColumn(tableName, columnName) { + return `ALTER TABLE ${this.quoteTable(tableName)} MODIFY ${columnName} DROP IDENTITY`; + } + + dropConstraintQuery(tableName, constraintName) { + return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${constraintName}`; + } + + setAutocommitQuery(value) { + if (value) { + // Do nothing, just for eslint + } + return ''; + } + + setIsolationLevelQuery(value, options) { + if (options.parent) { + return; + } + + switch (value) { + case Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED: + case Transaction.ISOLATION_LEVELS.READ_COMMITTED: + return 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED;'; + case Transaction.ISOLATION_LEVELS.REPEATABLE_READ: + // Serializable mode is equal to Snapshot Isolation (SI) + // defined in ANSI std. + return 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;'; + default: + throw new Error(`isolation level "${value}" is not supported`); + } + } + + startTransactionQuery(transaction) { + if (transaction.parent) { + return `SAVEPOINT "${transaction.name}"`; + } + + return 'BEGIN TRANSACTION'; + } + + commitTransactionQuery(transaction) { + if (transaction.parent) { + return; + } + + return 'COMMIT TRANSACTION'; + } + + rollbackTransactionQuery(transaction) { + if (transaction.parent) { + return `ROLLBACK TO SAVEPOINT "${transaction.name}"`; + } + + return 'ROLLBACK TRANSACTION'; + } + + selectFromTableFragment(options, model, attributes, tables, mainTableAs) { + this._throwOnEmptyAttributes(attributes, { modelName: model && model.name, as: mainTableAs }); + let mainFragment = `SELECT ${attributes.join(', ')} FROM ${tables}`; + + if (mainTableAs) { + mainFragment += ` ${mainTableAs}`; + } + + return mainFragment; + } + + handleSequelizeMethod(smth, tableName, factory, options, prepend) { + let str; + if (smth instanceof Utils.Json) { + // Parse nested object + if (smth.conditions) { + const conditions = this.parseConditionObject(smth.conditions).map(condition => + `${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'` + ); + + return conditions.join(' AND '); + } + if (smth.path) { + + // Allow specifying conditions using the sqlite json functions + if (this._checkValidJsonStatement(smth.path)) { + str = smth.path; + } else { + // Also support json property accessors + const paths = _.toPath(smth.path); + const column = paths.shift(); + str = this.jsonPathExtractionQuery(column, paths); + } + if (smth.value) { + str += util.format(' = %s', this.escape(smth.value)); + } + + return str; + } + } + if (smth instanceof Utils.Cast) { + if (smth.val instanceof Utils.SequelizeMethod) { + str = this.handleSequelizeMethod(smth.val, tableName, factory, options, prepend); + if (smth.type === 'boolean') { + str = `(CASE WHEN ${str}='true' THEN 1 ELSE 0 END)`; + return `CAST(${str} AS NUMBER)`; + } if (smth.type === 'timestamptz' && /json_value\(/.test(str)) { + str = str.slice(0, -1); + return `${str} RETURNING TIMESTAMP WITH TIME ZONE)`; + } + } + } + return super.handleSequelizeMethod(smth, tableName, factory, options, prepend); + } + + _checkValidJsonStatement(stmt) { + if (typeof stmt !== 'string') { + return false; + } + + let currentIndex = 0; + let openingBrackets = 0; + let closingBrackets = 0; + let hasJsonFunction = false; + let hasInvalidToken = false; + + while (currentIndex < stmt.length) { + const string = stmt.substr(currentIndex); + const functionMatches = JSON_FUNCTION_REGEX.exec(string); + if (functionMatches) { + currentIndex += functionMatches[0].indexOf('('); + hasJsonFunction = true; + continue; + } + + const operatorMatches = JSON_OPERATOR_REGEX.exec(string); + if (operatorMatches) { + currentIndex += operatorMatches[0].length; + hasJsonFunction = true; + continue; + } + + const tokenMatches = TOKEN_CAPTURE_REGEX.exec(string); + if (tokenMatches) { + const capturedToken = tokenMatches[1]; + if (capturedToken === '(') { + openingBrackets++; + } else if (capturedToken === ')') { + closingBrackets++; + } else if (capturedToken === ';') { + hasInvalidToken = true; + break; + } + currentIndex += tokenMatches[0].length; + continue; + } + + break; + } + + // Check invalid json statement + if (hasJsonFunction && (hasInvalidToken || openingBrackets !== closingBrackets)) { + throw new Error(`Invalid json statement: ${stmt}`); + } + + // return true if the statement has valid json function + return hasJsonFunction; + } + + jsonPathExtractionQuery(column, path) { + let paths = _.toPath(path); + const quotedColumn = this.isIdentifierQuoted(column) ? column : this.quoteIdentifier(column); + + paths = paths.map(subPath => { + return /\D/.test(subPath) ? Utils.addTicks(subPath, '"') : subPath; + }); + + const pathStr = this.escape(['$'].concat(paths).join('.').replace(/\.(\d+)(?:(?=\.)|$)/g, (__, digit) => `[${digit}]`)); + + return `json_value(${quotedColumn},${pathStr})`; + } + + addLimitAndOffset(options, model) { + let fragment = ''; + const offset = options.offset || 0, + isSubQuery = options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation; + + let orders = {}; + if (options.order) { + orders = this.getQueryOrders(options, model, isSubQuery); + } + + if (options.limit || options.offset) { + if (!(options.order && options.group) && (!options.order || options.include && !orders.subQueryOrder.length)) { + fragment += options.order && !isSubQuery ? ', ' : ' ORDER BY '; + fragment += `${this.quoteTable(options.tableAs || model.name) }.${this.quoteIdentifier(model.primaryKeyField)}`; + } + + if (options.offset || options.limit) { + fragment += ` OFFSET ${this.escape(offset)} ROWS`; + } + + if (options.limit) { + fragment += ` FETCH NEXT ${this.escape(options.limit)} ROWS ONLY`; + } + } + + return fragment; + } + + booleanValue(value) { + return value ? 1 : 0; + } + + quoteIdentifier(identifier, force) { + const optForceQuote = force || false; + const optQuoteIdentifiers = this.options.quoteIdentifiers !== false; + const rawIdentifier = Utils.removeTicks(identifier, '"'); + const regExp = (/^(([\w][\w\d_]*))$/g); + + if ( + optForceQuote !== true && + optQuoteIdentifiers === false && + regExp.test(rawIdentifier) === true && + !ORACLE_RESERVED_WORDS.includes(rawIdentifier.toUpperCase()) + ) { + // In Oracle, if tables, attributes or alias are created double-quoted, + // they are always case sensitive. If they contain any lowercase + // characters, they must always be double-quoted otherwise it + // would get uppercased by the DB. + // Here, we strip quotes if we don't want case sensitivity. + return rawIdentifier; + } + return Utils.addTicks(rawIdentifier, '"'); + } + + /** + * It causes bindbyPosition like :1, :2, :3 + * We pass the val parameter so that the outBind indexes + * starts after the inBind indexes end + * + * @param {Array} bind + * @param {number} posOffset + */ + bindParam(bind, posOffset = 0) { + return value => { + bind.push(value); + return `:${bind.length + posOffset}`; + }; + } +} + +// private methods +function wrapSingleQuote(identifier) { + return Utils.addTicks(identifier, "'"); +} + +/* istanbul ignore next */ +function throwMethodUndefined(methodName) { + throw new Error(`The method "${methodName}" is not defined! Please add it to your sql dialect.`); +} + +module.exports = OracleQueryGenerator; diff --git a/src/dialects/oracle/query-interface.js b/src/dialects/oracle/query-interface.js new file mode 100644 index 000000000000..2ce9bcdc9696 --- /dev/null +++ b/src/dialects/oracle/query-interface.js @@ -0,0 +1,87 @@ +// Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved + +'use strict'; +const { QueryInterface } = require('../abstract/query-interface'); +const QueryTypes = require('../../query-types'); + +const _ = require('lodash'); +/** + * The interface that Sequelize uses to talk with Oracle database + */ +class OracleQueryInterface extends QueryInterface { + + /** + * Upsert + * + * @param {string} tableName table to upsert on + * @param {object} insertValues values to be inserted, mapped to field name + * @param {object} updateValues values to be updated, mapped to field name + * @param {object} where where conditions, which can be used for UPDATE part when INSERT fails + * @param {object} options query options + * + * @returns {Promise} Resolves an array with + */ + async upsert(tableName, insertValues, updateValues, where, options) { + options = { ...options }; + + const model = options.model; + const primaryKeys = Object.values(model.primaryKeys).map(item => item.field); + const uniqueKeys = Object.values(model.uniqueKeys).filter(c => c.fields.length > 0).map(c => c.fields); + const indexKeys = Object.values(model._indexes).filter(c => c.unique && c.fields.length > 0).map(c => c.fields); + + options.type = QueryTypes.UPSERT; + options.updateOnDuplicate = Object.keys(updateValues); + options.upsertKeys = []; + + // For fields in updateValues, try to find a constraint or unique index + // that includes given field. Only first matching upsert key is used. + for (const field of options.updateOnDuplicate) { + const uniqueKey = uniqueKeys.find(fields => fields.includes(field)); + if (uniqueKey) { + options.upsertKeys = uniqueKey; + break; + } + + const indexKey = indexKeys.find(fields => fields.includes(field)); + if (indexKey) { + options.upsertKeys = indexKey; + break; + } + } + + // Always use PK, if no constraint available OR update data contains PK + if ( + options.upsertKeys.length === 0 + || _.intersection(options.updateOnDuplicate, primaryKeys).length + ) { + options.upsertKeys = primaryKeys; + } + + options.upsertKeys = _.uniq(options.upsertKeys); + + let whereHasNull = false; + + primaryKeys.forEach(element => { + if (where[element] === null) { + whereHasNull = true; + } + }); + + if (whereHasNull === true) { + where = options.upsertKeys.reduce((result, attribute) => { + result[attribute] = insertValues[attribute]; + return result; + }, {}); + } + + const sql = this.queryGenerator.upsertQuery(tableName, insertValues, updateValues, where, model, options); + // we need set this to undefined otherwise sequelize would raise an error + // Error: Both `sql.bind` and `options.bind` cannot be set at the same time + if (sql.bind) { + options.bind = undefined; + } + return await this.sequelize.query(sql, options); + } +} + +exports.OracleQueryInterface = OracleQueryInterface; diff --git a/src/dialects/oracle/query.js b/src/dialects/oracle/query.js new file mode 100644 index 000000000000..0dec9c953a15 --- /dev/null +++ b/src/dialects/oracle/query.js @@ -0,0 +1,672 @@ +// Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved + +'use strict'; + +const AbstractQuery = require('../abstract/query'); +const SequelizeErrors = require('../../errors'); +const parserStore = require('../parserStore')('oracle'); +const _ = require('lodash'); +const Utils = require('../../utils'); +const { logger } = require('../../utils/logger'); + +const debug = logger.debugContext('sql:oracle'); + +class Query extends AbstractQuery { + constructor(connection, sequelize, options) { + super(connection, sequelize, options); + this.options = _.extend( + { + logging: console.log, + plain: false, + raw: false + }, + options || {} + ); + + this.checkLoggingOption(); + this.outFormat = options.outFormat || this.sequelize.connectionManager.lib.OBJECT; + } + + getInsertIdField() { + return 'id'; + } + + getExecOptions() { + const self = this; + const execOpts = { outFormat: self.outFormat, autoCommit: self.autoCommit }; + + // We set the oracledb + const oracledb = self.sequelize.connectionManager.lib; + + if (this.isSelectQuery() && this.model) { + const fInfo = {}; + const keys = Object.keys(this.model.tableAttributes); + keys.forEach( key => { + const keyValue = this.model.tableAttributes[key]; + if ( keyValue.type.key === 'DECIMAL') { + fInfo[key] = { type: oracledb.STRING }; + } + }); + + if ( fInfo ) { + execOpts.fetchInfo = fInfo; + } + } + return execOpts; + } + _run(connection, sql, parameters) { + const self = this; + // We set the oracledb + const oracledb = self.sequelize.connectionManager.lib; + // We remove the / that escapes quotes + if (sql.match(/^(SELECT|INSERT|DELETE)/)) { + this.sql = sql.replace(/; *$/, ''); + } else { + this.sql = sql; + } + const complete = this._logQuery(sql, debug, parameters); + const outParameters = []; + const bindParameters = []; + const bindDef = []; + + // When this.options.bindAttributes exists then it is an insertQuery/upsertQuery + // So we insert the return bind direction and type + if (parameters !== undefined && this.options.outBindAttributes) { + outParameters.push(...Object.values(this.options.outBindAttributes)); + // For upsertQuery we need to push the bindDef for isUpdate + if (this.isUpsertQuery()) { + outParameters.push({ dir: oracledb.BIND_OUT }); + } + } + + this.bindParameters = outParameters; + // construct input binds from parameters for single row insert execute call + // ex: [3, 4,...] + if (parameters !== undefined && typeof parameters === 'object') { + if (this.options.executeMany) { + // Constructing BindDefs for ExecuteMany call + // Building the bindDef for in and out binds + bindDef.push(...Object.values(this.options.inbindAttributes)); + bindDef.push(...outParameters); + this.bindParameters = parameters; + } else { + Object.values(parameters).forEach(value => { + bindParameters.push(value); + }); + bindParameters.push(...outParameters); + Object.assign(this.bindParameters, bindParameters); + } + } + + // TRANSACTION SUPPORT + if (_.startsWith(self.sql, 'BEGIN TRANSACTION')) { + self.autocommit = false; + return Promise.resolve(); + } + if (_.startsWith(self.sql, 'SET AUTOCOMMIT ON')) { + self.autocommit = true; + return Promise.resolve(); + } + if (_.startsWith(self.sql, 'SET AUTOCOMMIT OFF')) { + self.autocommit = false; + return Promise.resolve(); + } + if (_.startsWith(self.sql, 'DECLARE x NUMBER')) { + // Calling a stored procedure for bulkInsert with NO attributes, returns nothing + if (self.autoCommit === undefined) { + if (connection.uuid) { + self.autoCommit = false; + } else { + self.autoCommit = true; + } + } + return connection + .execute(self.sql, this.bindParameters, { autoCommit: self.autoCommit }) + .then(() => { + return {}; + }) + .catch(error => { + throw self.formatError(error); + }) + .finally(() => { + complete(); + }); + } + if (_.startsWith(self.sql, 'BEGIN')) { + // Call to stored procedures - BEGIN TRANSACTION has been treated before + if (self.autoCommit === undefined) { + if (connection.uuid) { + self.autoCommit = false; + } else { + self.autoCommit = true; + } + } + + return connection + .execute(self.sql, self.bindParameters, { + outFormat: self.outFormat, + autoCommit: self.autoCommit + }) + .then(result => { + if (!Array.isArray(result.outBinds)) { + return [result.outBinds]; + } + return result.outBinds; + }) + .catch(error => { + throw self.formatError(error); + }) + .finally(() => { + complete(); + }); + } + if (_.startsWith(self.sql, 'COMMIT TRANSACTION')) { + return connection + .commit() + .then(() => { + return {}; + }) + .catch(err => { + throw self.formatError(err); + }) + .finally(() => { + complete(); + }); + } + if (_.startsWith(self.sql, 'ROLLBACK TRANSACTION')) { + return connection + .rollback() + .then(() => { + return {}; + }) + .catch(err => { + throw self.formatError(err); + }) + .finally(() => { + complete(); + }); + } + if (_.startsWith(self.sql, 'SET TRANSACTION')) { + return connection + .execute(self.sql, [], { autoCommit: false }) + .then(() => { + return {}; + }) + .catch(error => { + throw self.formatError(error); + }) + .finally(() => { + complete(); + }); + } + // QUERY SUPPORT + // As Oracle does everything in transaction, if autoCommit is not defined, we set it to true + if (self.autoCommit === undefined) { + if (connection.uuid) { + self.autoCommit = false; + } else { + self.autoCommit = true; + } + } + + // inbind parameters added byname. merge them + if ('inputParameters' in self.options && self.options.inputParameters !== null) { + Object.assign(self.bindParameters, self.options.inputParameters); + } + const execOpts = this.getExecOptions(); + if (this.options.executeMany && bindDef.length > 0) { + execOpts.bindDefs = bindDef; + } + const executePromise = this.options.executeMany ? connection.executeMany(self.sql, self.bindParameters, execOpts) : connection.execute(self.sql, self.bindParameters, execOpts); + // If we have some mapping with parameters to do - INSERT queries + return executePromise + .then(result => { + return self.formatResults(result); + }) + .catch(error => { + throw self.formatError(error); + }) + .finally(() => { + complete(); + }); + + } + + run(sql, parameters) { + const self = this; + if (!sql.match(/END;$/)) { + sql = sql.replace(/; *$/, ''); + } + + return self._run(this.connection, sql, parameters); + } + + /** + * The parameters to query.run function are built here + * + * @param {string} sql + * @param {Array} values + * @param {string} dialect + */ + static formatBindParameters(sql, values, dialect) { + const replacementFunc = (match, key, values) => { + if (values[key] !== undefined) { + return `:${key}`; + } + return undefined; + }; + sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0]; + + return [sql, values]; + } + + /** + * Building the attribute map by matching the column names received + * from DB and the one in rawAttributes + * to sequelize format + * + * @param {object} attrsMap + * @param {object} rawAttributes + * @private + */ + _getAttributeMap(attrsMap, rawAttributes) { + attrsMap = Object.assign(attrsMap, _.reduce(rawAttributes, (mp, _, key) => { + const catalogKey = this.sequelize.queryInterface.queryGenerator.getCatalogName(key); + mp[catalogKey] = key; + return mp; + }, {})); + } + + /** + * Process rows received from the DB. + * Use parse function to parse the returned value + * to sequelize format + * + * @param {Array} rows + * @private + */ + _processRows(rows) { + let result = rows; + let attrsMap = {}; + + // When quoteIdentifiers is false we need to map the DB column names + // To the one in attribute list + if (this.sequelize.options.quoteIdentifiers === false) { + // Building the attribute map from this.options.attributes + // Needed in case of an aggregate function + attrsMap = _.reduce(this.options.attributes, (mp, v) => { + // Aggregate function is of form + // Fn {fn: 'min', min}, so we have the name in index one of the object + if (typeof v === 'object') { + v = v[1]; + } + const catalogv = this.sequelize.queryInterface.queryGenerator.getCatalogName(v); + mp[catalogv] = v; + return mp; + }, {}); + + + // Building the attribute map by matching the column names received + // from DB and the one in model.rawAttributes + if (this.model) { + this._getAttributeMap(attrsMap, this.model.rawAttributes); + } + + // If aliasesmapping exists we update the attribute map + if (this.options.aliasesMapping) { + const obj = Object.fromEntries(this.options.aliasesMapping); + rows = rows + .map(row => _.toPairs(row) + .reduce((acc, [key, value]) => { + const mapping = Object.values(obj).find(element => { + const catalogElement = this.sequelize.queryInterface.queryGenerator.getCatalogName(element); + return catalogElement === key; + }); + if (mapping) + acc[mapping || key] = value; + return acc; + }, {}) + ); + } + + // Modify the keys into the format that sequelize expects + result = rows.map(row => { + return _.mapKeys(row, (value, key) => { + const targetAttr = attrsMap[key]; + if (typeof targetAttr === 'string' && targetAttr !== key) { + return targetAttr; + } + return key; + }); + }); + } + + // We parse the value received from the DB based on its datatype + if (this.model) { + result = result.map(row => { + return _.mapValues(row, (value, key) => { + if (this.model.rawAttributes[key] && this.model.rawAttributes[key].type) { + let typeid = this.model.rawAttributes[key].type.toLocaleString(); + if (this.model.rawAttributes[key].type.key === 'JSON') { + value = JSON.parse(value); + } + // For some types, the "name" of the type is returned with the length, we remove it + // For Boolean we skip this because BOOLEAN is mapped to CHAR(1) and we dont' want to + // remove the (1) for BOOLEAN + if (typeid.indexOf('(') > -1 && this.model.rawAttributes[key].type.key !== 'BOOLEAN') { + typeid = typeid.substr(0, typeid.indexOf('(')); + } + const parse = parserStore.get(typeid); + if (value !== null & !!parse) { + value = parse(value); + } + } + return value; + }); + }); + } + + return result; + } + + /** + * High level function that handles the results of a query execution. + * Example: + * Oracle format : + * { rows: //All rows + [ [ 'Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production' ], + [ 'PL/SQL Release 11.2.0.1.0 - Production' ], + [ 'CORE\t11.2.0.1.0\tProduction' ], + [ 'TNS for 64-bit Windows: Version 11.2.0.1.0 - Production' ], + [ 'NLSRTL Version 11.2.0.1.0 - Production' ] ], + resultSet: undefined, + outBinds: undefined, //Used for dbms_put.line + rowsAffected: undefined, //Number of rows affected + metaData: [ { name: 'BANNER' } ] } + * + * @param {Array} data - The result of the query execution. + */ + formatResults(data) { + let result = this.instance; + if (this.isInsertQuery(data)) { + let insertData; + if (data.outBinds) { + const keys = Object.keys(this.options.outBindAttributes); + insertData = data.outBinds; + // For one row insert out bind array is 1D array + // we convert it to 2D array for uniformity + if (this.instance) { + insertData = [insertData]; + } + // Mapping the bind parameter to their values + const res = insertData.map(row =>{ + const obj = {}; + row.forEach((element, index) =>{ + obj[keys[index]] = element[0]; + }); + return obj; + }); + insertData = res; + // For bulk insert this.insert is undefined + // we map result to res, for one row insert + // result needs to be this.instance + if (!this.instance) { + result = res; + } + } + this.handleInsertQuery(insertData); + return [result, data.rowsAffected]; + } + if (this.isShowTablesQuery()) { + result = this.handleShowTablesQuery(data.rows); + } else if (this.isDescribeQuery()) { + result = {}; + // Getting the table name on which we are doing describe query + const table = Object.keys(this.sequelize.models); + const modelAttributes = {}; + // Get the model raw attributes + if (this.sequelize.models && table.length > 0) { + this._getAttributeMap(modelAttributes, this.sequelize.models[table[0]].rawAttributes); + } + data.rows.forEach(_result => { + if (_result.Default) { + _result.Default = _result.Default.replace("('", '') + .replace("')", '') + .replace(/'/g, ''); /* jshint ignore: line */ + } + + if (!(modelAttributes[_result.COLUMN_NAME] in result)) { + let key = modelAttributes[_result.COLUMN_NAME]; + if (!key) { + key = _result.COLUMN_NAME; + } + + result[key] = { + type: _result.DATA_TYPE.toUpperCase(), + allowNull: _result.NULLABLE === 'N' ? false : true, + defaultValue: undefined, + primaryKey: _result.PRIMARY === 'PRIMARY' + }; + } + }); + } else if (this.isShowIndexesQuery()) { + result = this.handleShowIndexesQuery(data.rows); + } else if (this.isSelectQuery()) { + const rows = data.rows; + const result = this._processRows(rows); + return this.handleSelectQuery(result); + } else if (this.isCallQuery()) { + result = data.rows[0]; + } else if (this.isUpdateQuery()) { + result = [result, data.rowsAffected]; + } else if (this.isBulkUpdateQuery()) { + result = data.rowsAffected; + } else if (this.isBulkDeleteQuery()) { + result = data.rowsAffected; + } else if (this.isVersionQuery()) { + const version = data.rows[0].VERSION; + if (version) { + const versions = version.split('.'); + result = `${versions[0]}.${versions[1]}.${versions[2]}`; + } else { + result = '0.0.0'; + } + } else if (this.isForeignKeysQuery()) { + result = data.rows; + } else if (this.isUpsertQuery()) { + // Upsert Query, will return nothing + data = data.outBinds; + const keys = Object.keys(this.options.outBindAttributes); + const obj = {}; + for (const k in keys) { + obj[keys[k]] = data[k]; + } + obj.isUpdate = data[data.length - 1]; + data = obj; + result = [{ isNewRecord: data.isUpdate, value: data }, data.isUpdate == 0]; + } else if (this.isShowConstraintsQuery()) { + result = this.handleShowConstraintsQuery(data); + } else if (this.isRawQuery()) { + // If data.rows exists then it is a select query + // Hence we would have two components + // metaData and rows and we return them + // as [data.rows, data.metaData] + // Else it is result of update/upsert/insert query + // and it has no rows so we return [data, data] + if (data && data.rows) { + return [data.rows, data.metaData]; + } + return [data, data]; + } + + return result; + } + + handleShowConstraintsQuery(data) { + // Convert snake_case keys to camelCase as its generated by stored procedure + return data.rows.map(result => { + const constraint = {}; + for (const key in result) { + constraint[_.camelCase(key)] = result[key].toLowerCase(); + } + return constraint; + }); + } + + handleShowTablesQuery(results) { + return results.map(resultSet => { + return { + tableName: resultSet.TABLE_NAME, + schema: resultSet.TABLE_SCHEMA + }; + }); + } + + formatError(err) { + let match; + // ORA-00001: unique constraint (USER.XXXXXXX) violated + match = err.message.match(/unique constraint ([\s\S]*) violated/); + if (match && match.length > 1) { + match[1] = match[1].replace('(', '').replace(')', '').split('.')[1]; // As we get (SEQUELIZE.UNIQNAME), we replace to have UNIQNAME + const errors = []; + let fields = [], + message = 'Validation error', + uniqueKey = null; + + if (this.model) { + const uniqueKeys = Object.keys(this.model.uniqueKeys); + + const currKey = uniqueKeys.find(key => { + // We check directly AND with quotes -> "a"" === a || "a" === "a" + return key.toUpperCase() === match[1].toUpperCase() || key.toUpperCase() === `"${match[1].toUpperCase()}"`; + }); + + if (currKey) { + uniqueKey = this.model.uniqueKeys[currKey]; + fields = uniqueKey.fields; + } + + if (uniqueKey && !!uniqueKey.msg) { + message = uniqueKey.msg; + } + + fields.forEach(field => { + errors.push( + new SequelizeErrors.ValidationErrorItem( + this.getUniqueConstraintErrorMessage(field), + 'unique violation', + field, + null + ) + ); + }); + } + + return new SequelizeErrors.UniqueConstraintError({ + message, + errors, + err, + fields + }); + } + + // ORA-02291: integrity constraint (string.string) violated - parent key not found / ORA-02292: integrity constraint (string.string) violated - child record found + match = err.message.match(/ORA-02291/) || err.message.match(/ORA-02292/); + if (match && match.length > 0) { + return new SequelizeErrors.ForeignKeyConstraintError({ + fields: null, + index: match[1], + parent: err + }); + } + + // ORA-02443: Cannot drop constraint - nonexistent constraint + match = err.message.match(/ORA-02443/); + if (match && match.length > 0) { + return new SequelizeErrors.UnknownConstraintError(match[1]); + } + + return new SequelizeErrors.DatabaseError(err); + } + + isShowIndexesQuery() { + return this.sql.indexOf('SELECT i.index_name,i.table_name, i.column_name, u.uniqueness') > -1; + } + + isSelectCountQuery() { + return this.sql.toUpperCase().indexOf('SELECT COUNT(') > -1; + } + + handleShowIndexesQuery(data) { + const acc = []; + + // We first treat the datas + data.forEach(indexRecord => { + // We create the object + if (!acc[indexRecord.INDEX_NAME]) { + acc[indexRecord.INDEX_NAME] = { + unique: indexRecord.UNIQUENESS === 'UNIQUE' ? true : false, + primary: indexRecord.INDEX_NAME.toLowerCase().indexOf('pk') === 0, + name: indexRecord.INDEX_NAME.toLowerCase(), + tableName: indexRecord.TABLE_NAME.toLowerCase(), + type: undefined + }; + acc[indexRecord.INDEX_NAME].fields = []; + } + + // We create the fields + acc[indexRecord.INDEX_NAME].fields.push({ + attribute: indexRecord.COLUMN_NAME, + length: undefined, + order: indexRecord.DESCEND, + collate: undefined + }); + }); + const returnIndexes = []; + + const accKeys = Object.keys(acc); + accKeys.forEach(accKey => { + const columns = {}; + columns.fields = acc[accKey].fields; + // We are generating index field name in the format sequelize expects + // to avoid creating a unique index on primary key column + if (acc[accKey].primary === true) { + acc[accKey].name = Utils.nameIndex(columns, acc[accKey].tableName).name; + } + returnIndexes.push(acc[accKey]); + }); + + return returnIndexes; + } + + handleInsertQuery(results, metaData) { + if (this.instance && results.length > 0) { + if ('pkReturnVal' in results[0]) { + // The PK of the table is a reserved word (ex : uuid), we have to change the name in the result for the model to find the value correctly + results[0][this.model.primaryKeyAttribute] = results[0].pkReturnVal; + delete results[0].pkReturnVal; + } + // add the inserted row id to the instance + const autoIncrementField = this.model.autoIncrementAttribute; + let autoIncrementFieldAlias = null, + id = null; + + if ( + Object.prototype.hasOwnProperty.call(this.model.rawAttributes, autoIncrementField) && + this.model.rawAttributes[autoIncrementField].field !== undefined + ) + autoIncrementFieldAlias = this.model.rawAttributes[autoIncrementField].field; + + id = id || results && results[0][this.getInsertIdField()]; + id = id || metaData && metaData[this.getInsertIdField()]; + id = id || results && results[0][autoIncrementField]; + id = id || autoIncrementFieldAlias && results && results[0][autoIncrementFieldAlias]; + + this.instance[autoIncrementField] = id; + } + } +} + +module.exports = Query; +module.exports.Query = Query; +module.exports.default = Query; diff --git a/src/model.js b/src/model.js index 91d2b3548022..94426e006f28 100644 --- a/src/model.js +++ b/src/model.js @@ -2644,7 +2644,7 @@ class Model { } } - if (options.ignoreDuplicates && ['mssql', 'db2'].includes(dialect)) { + if (options.ignoreDuplicates && ['mssql', 'db2', 'oracle'].includes(dialect)) { throw new Error(`${dialect} does not support the ignoreDuplicates option.`); } if (options.updateOnDuplicate && (dialect !== 'mysql' && dialect !== 'mariadb' && dialect !== 'sqlite' && dialect !== 'postgres')) { diff --git a/src/sequelize.js b/src/sequelize.js index f89002d753f9..5ea8d7a821c0 100644 --- a/src/sequelize.js +++ b/src/sequelize.js @@ -340,6 +340,9 @@ class Sequelize { case 'mysql': Dialect = require('./dialects/mysql'); break; + case 'oracle': + Dialect = require('./dialects/oracle'); + break; case 'postgres': Dialect = require('./dialects/postgres'); break; @@ -353,7 +356,7 @@ class Sequelize { Dialect = require('./dialects/snowflake'); break; default: - throw new Error(`The dialect ${this.getDialect()} is not supported. Supported dialects: mssql, mariadb, mysql, postgres, db2 and sqlite.`); + throw new Error(`The dialect ${this.getDialect()} is not supported. Supported dialects: mssql, mariadb, mysql, oracle, postgres, db2 and sqlite.`); } this.dialect = new Dialect(this); @@ -960,7 +963,12 @@ class Sequelize { ...options }; - await this.query('SELECT 1+1 AS result', options); + let sql = 'SELECT 1+1 AS result'; + if (this.dialect.name === 'oracle') { + sql += ' FROM DUAL'; + } + + await this.query(sql, options); return; } diff --git a/src/sql-string.js b/src/sql-string.js index d56c0468b300..c7146478e5ab 100644 --- a/src/sql-string.js +++ b/src/sql-string.js @@ -28,7 +28,7 @@ function escape(val, timeZone, dialect, format) { // SQLite doesn't have true/false support. MySQL aliases true/false to 1/0 // for us. Postgres actually has a boolean type with true/false literals, // but sequelize doesn't use it yet. - if (['sqlite', 'mssql'].includes(dialect)) { + if (['sqlite', 'mssql', 'oracle'].includes(dialect)) { return +!!val; } return (!!val).toString(); @@ -74,6 +74,13 @@ function escape(val, timeZone, dialect, format) { // null character is not allowed in Postgres val = val.replace(/\0/g, '\\0'); } + } else if (dialect === 'oracle' && (val.indexOf('TO_TIMESTAMP') > -1 || val.indexOf('TO_DATE') > -1 || typeof val === 'string')) { + if (val.indexOf('TO_TIMESTAMP') > -1 || val.indexOf('TO_DATE') > -1) { + return val; + } + if (typeof val === 'string') { + val = val.replace(/'/g, "''"); + } } else { // eslint-disable-next-line no-control-regex diff --git a/src/utils.js b/src/utils.js index 3ac0bd421c5e..9bc431002c74 100644 --- a/src/utils.js +++ b/src/utils.js @@ -336,7 +336,7 @@ function removeNullValuesFromHash(hash, omitNull, options) { } exports.removeNullValuesFromHash = removeNullValuesFromHash; -const dialects = new Set(['mariadb', 'mysql', 'postgres', 'sqlite', 'mssql', 'db2']); +const dialects = new Set(['mariadb', 'mysql', 'postgres', 'sqlite', 'mssql', 'db2', 'oracle']); function now(dialect) { const d = new Date(); diff --git a/test/config/config.js b/test/config/config.js index f914d04e8efd..20796e26c9be 100644 --- a/test/config/config.js +++ b/test/config/config.js @@ -81,5 +81,17 @@ module.exports = { max: process.env.SEQ_DB2_POOL_MAX || process.env.SEQ_POOL_MAX || 5, idle: process.env.SEQ_DB2_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000 } + }, + + oracle: { + database: env.SEQ_ORACLE_DB || env.SEQ_DB || 'XEPDB1', + username: env.SEQ_ORACLE_USER || env.SEQ_USER || 'sequelizetest', + password: env.SEQ_ORACLE_PW || env.SEQ_PW || 'sequelizepassword', + host: env.SEQ_ORACLE_HOST || env.SEQ_HOST || '127.0.0.1', + port: env.SEQ_ORACLE_PORT || env.SEQ_PORT || 1521, + pool: { + max: env.SEQ_ORACLE_POOL_MAX || env.SEQ_POOL_MAX || 5, + idle: env.SEQ_ORACLE_POOL_IDLE || env.SEQ_POOL_IDLE || 3000 + } } }; diff --git a/test/integration/associations/belongs-to-many.test.js b/test/integration/associations/belongs-to-many.test.js index 0425e06c2397..675e310d8bba 100644 --- a/test/integration/associations/belongs-to-many.test.js +++ b/test/integration/associations/belongs-to-many.test.js @@ -1417,7 +1417,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { describe('hasAssociations with binary key', () => { beforeEach(function() { - const keyDataType = ['mysql', 'mariadb', 'db2'].includes(dialect) ? 'BINARY(255)' : DataTypes.BLOB('tiny'); + const keyDataType = ['mysql', 'mariadb', 'db2'].includes(dialect) ? 'BINARY(255)' : dialect === 'oracle' ? DataTypes.STRING(255, true) : DataTypes.BLOB('tiny'); this.Article = this.sequelize.define('Article', { id: { type: keyDataType, @@ -3115,7 +3115,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { await this.sequelize.sync({ force: true }); let result = await this.sequelize.getQueryInterface().showAllTables(); - if (['mssql', 'mariadb', 'db2'].includes(dialect)) { + if (['mssql', 'mariadb', 'db2', 'oracle'].includes(dialect)) { result = result.map(v => v.tableName); } @@ -3132,7 +3132,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { await this.sequelize.sync({ force: true }); let result = await this.sequelize.getQueryInterface().showAllTables(); - if (['mssql', 'mariadb', 'db2'].includes(dialect)) { + if (['mssql', 'mariadb', 'db2', 'oracle'].includes(dialect)) { result = result.map(v => v.tableName); } diff --git a/test/integration/associations/belongs-to.test.js b/test/integration/associations/belongs-to.test.js index 6c4506f3e06e..163fffc32a4d 100644 --- a/test/integration/associations/belongs-to.test.js +++ b/test/integration/associations/belongs-to.test.js @@ -626,7 +626,8 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { // NOTE: mssql does not support changing an autoincrement primary key if (Support.getTestDialect() !== 'mssql' && - Support.getTestDialect() !== 'db2') { + Support.getTestDialect() !== 'db2' && + Support.getTestDialect() !== 'oracle') { it('can cascade updates', async function() { const Task = this.sequelize.define('Task', { title: DataTypes.STRING }), User = this.sequelize.define('User', { username: DataTypes.STRING }); diff --git a/test/integration/associations/has-many.test.js b/test/integration/associations/has-many.test.js index 340fa3494bc9..4d3a1408ce10 100644 --- a/test/integration/associations/has-many.test.js +++ b/test/integration/associations/has-many.test.js @@ -1102,7 +1102,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { }); // NOTE: mssql does not support changing an autoincrement primary key - if (dialect !== 'mssql' && dialect !== 'db2') { + if (dialect !== 'mssql' && dialect !== 'db2' && dialect !== 'oracle') { it('can cascade updates', async function() { const Task = this.sequelize.define('Task', { title: DataTypes.STRING }), User = this.sequelize.define('User', { username: DataTypes.STRING }); diff --git a/test/integration/associations/has-one.test.js b/test/integration/associations/has-one.test.js index e68cf25539a6..5535db9fa7c6 100644 --- a/test/integration/associations/has-one.test.js +++ b/test/integration/associations/has-one.test.js @@ -454,7 +454,8 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { // NOTE: mssql does not support changing an autoincrement primary key if (Support.getTestDialect() !== 'mssql' && - Support.getTestDialect() !== 'db2') { + Support.getTestDialect() !== 'db2' && + Support.getTestDialect() !== 'oracle') { it('can cascade updates', async function() { const Task = this.sequelize.define('Task', { title: Sequelize.STRING }), User = this.sequelize.define('User', { username: Sequelize.STRING }); diff --git a/test/integration/cls.test.js b/test/integration/cls.test.js index 746d43996842..f84c6f4d59e8 100644 --- a/test/integration/cls.test.js +++ b/test/integration/cls.test.js @@ -142,7 +142,7 @@ if (current.dialect.supports.transactions) { it('promises returned by sequelize.query are correctly patched', async function() { await this.sequelize.transaction(async t => { - await this.sequelize.query('select 1', { type: Sequelize.QueryTypes.SELECT }); + await this.sequelize.query(`select 1${ Support.addDualInSelect()}`, { type: Sequelize.QueryTypes.SELECT }); return expect(this.ns.get('transaction')).to.equal(t); } ); @@ -160,12 +160,12 @@ if (current.dialect.supports.transactions) { const result = this.ns.runPromise(async () => { this.ns.set('value', 1); await delay(500); - return sequelize.query('select 1;'); + return sequelize.query(`select 1${ Support.addDualInSelect() };`); }); await this.ns.runPromise(() => { this.ns.set('value', 2); - return sequelize.query('select 2;'); + return sequelize.query(`select 2${ Support.addDualInSelect() };`); }); await result; diff --git a/test/integration/configuration.test.js b/test/integration/configuration.test.js index 4a7fa5cd11d5..f468b69e6604 100644 --- a/test/integration/configuration.test.js +++ b/test/integration/configuration.test.js @@ -65,8 +65,9 @@ describe(Support.getTestDialectTeaser('Configuration'), () => { else if (dialect === 'db2') { await expect(seq.query('select 1 as hello')).to.eventually.be.rejectedWith(...willBeRejectedWithArgs); } - - else { + else if (dialect === 'oracle') { + await expect(seq.query('select 1 as hello FROM DUAL')).to.eventually.be.rejectedWith(Sequelize.ConnectionRefusedError); + } else { await expect(seq.query('select 1 as hello')).to.eventually.be.rejectedWith(Sequelize.ConnectionRefusedError, 'connect ECONNREFUSED'); } }); @@ -74,7 +75,7 @@ describe(Support.getTestDialectTeaser('Configuration'), () => { it('when we don\'t have a valid dialect.', () => { expect(() => { new Sequelize(config[dialect].database, config[dialect].username, config[dialect].password, { host: '0.0.0.1', port: config[dialect].port, dialect: 'some-fancy-dialect' }); - }).to.throw(Error, 'The dialect some-fancy-dialect is not supported. Supported dialects: mssql, mariadb, mysql, postgres, db2 and sqlite.'); + }).to.throw(Error, 'The dialect some-fancy-dialect is not supported. Supported dialects: mssql, mariadb, mysql, oracle, postgres, db2 and sqlite.'); }); }); diff --git a/test/integration/data-types.test.js b/test/integration/data-types.test.js index 6899d26e2b59..cb2430a533ec 100644 --- a/test/integration/data-types.test.js +++ b/test/integration/data-types.test.js @@ -33,6 +33,22 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { return value.format('YYYY-MM-DD HH:mm:ss'); }); + // oracle has a _bindParam function that checks if DATE was created with + // the boolean param (if so it outputs a Buffer bind param). This override + // isn't needed for other dialects + let bindParam; + if (dialect === 'oracle') { + bindParam = Sequelize.DATE.prototype.bindParam = sinon.spy(function(value, options) { + if (!moment.isMoment(value)) { + value = this._applyTimezone(value, options); + } + // For the Oracle dialect, use TO_DATE() + const formatedDate = value.format('YYYY-MM-DD HH:mm:ss'); + const format = 'YYYY-MM-DD HH24:mi:ss'; + return `TO_DATE('${formatedDate}', '${format}')`; + }); + } + current.refreshTypes(); const User = current.define('user', { @@ -50,7 +66,9 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { const obj = await User.findAll(); const user = obj[0]; expect(parse).to.have.been.called; - expect(stringify).to.have.been.called; + // For the Oracle dialect we check if bindParam was called + // for other dalects we check if stringify was called + dialect === 'oracle' ? expect(bindParam).to.have.been.called : expect(stringify).to.have.been.called; expect(moment.isMoment(user.dateField)).to.be.ok; @@ -115,7 +133,14 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { it('calls parse and stringify for JSON', async () => { const Type = new Sequelize.JSON(); - await testSuccess(Type, { test: 42, nested: { foo: 'bar' } }); + // oracle has a _bindParam function that checks if JSON was created with + // the boolean param (if so it outputs a Buffer bind param). This override + // isn't needed for other dialects + if (dialect === 'oracle') { + await testSuccess(Type, { test: 42, nested: { foo: 'bar' } }, { useBindParam: true }); + } else { + await testSuccess(Type, { test: 42, nested: { foo: 'bar' } }); + } }); } @@ -146,19 +171,38 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { it('calls parse and stringify for DATE', async () => { const Type = new Sequelize.DATE(); - await testSuccess(Type, new Date()); + // oracle has a _bindParam function that checks if DATE was created with + // the boolean param (if so it outputs a Buffer bind param). This override + // isn't needed for other dialects + if (dialect === 'oracle') { + await testSuccess(Type, new Date(), { useBindParam: true }); + } else { + await testSuccess(Type, new Date()); + } }); it('calls parse and stringify for DATEONLY', async () => { const Type = new Sequelize.DATEONLY(); - await testSuccess(Type, moment(new Date()).format('YYYY-MM-DD')); + // oracle has a _bindParam function that checks if DATEONLY was created with + // the boolean param (if so it outputs a Buffer bind param). This override + // isn't needed for other dialects + if (dialect === 'oracle') { + await testSuccess(Type, moment(new Date()).format('YYYY-MM-DD'), { useBindParam: true }); + } else { + await testSuccess(Type, moment(new Date()).format('YYYY-MM-DD')); + } }); it('calls parse and stringify for TIME', async () => { const Type = new Sequelize.TIME(); - await testSuccess(Type, moment(new Date()).format('HH:mm:ss')); + // TIME Datatype isn't supported by the oracle dialect + if (dialect === 'oracle') { + testFailure(Type); + } else { + await testSuccess(Type, moment(new Date()).format('HH:mm:ss')); + } }); it('calls parse and stringify for BLOB', async () => { @@ -170,16 +214,23 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { it('calls parse and stringify for CHAR', async () => { const Type = new Sequelize.CHAR(); - await testSuccess(Type, 'foobar'); + // oracle has a _bindParam function that checks if STRING was created with + // the boolean param (if so it outputs a Buffer bind param). This override + // isn't needed for other dialects + if (dialect === 'oracle') { + await testSuccess(Type, 'foobar', { useBindParam: true }); + } else { + await testSuccess(Type, 'foobar'); + } }); it('calls parse and stringify/bindParam for STRING', async () => { const Type = new Sequelize.STRING(); - // mssql has a _bindParam function that checks if STRING was created with + // mssql/oracle has a _bindParam function that checks if STRING was created with // the boolean param (if so it outputs a Buffer bind param). This override // isn't needed for other dialects - if (dialect === 'mssql' || dialect === 'db2') { + if (dialect === 'mssql' || dialect === 'db2' || dialect === 'oracle') { await testSuccess(Type, 'foobar', { useBindParam: true }); } else { await testSuccess(Type, 'foobar'); @@ -226,7 +277,8 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { } }); - it('should handle JS BigInt type', async function() { + // Node-oracledb doesn't support JS BigInt yet + (dialect !== 'oracle' ? it : it.skip)('should handle JS BigInt type', async function() { const User = this.sequelize.define('user', { age: Sequelize.BIGINT }); @@ -308,7 +360,7 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { const Type = new Sequelize.UUID(); // there is no dialect.supports.UUID yet - if (['postgres', 'sqlite', 'db2'].includes(dialect)) { + if (['postgres', 'sqlite', 'oracle', 'db2'].includes(dialect)) { await testSuccess(Type, uuid.v4()); } else { // No native uuid type @@ -377,7 +429,7 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { it('calls parse and stringify for ENUM', async () => { const Type = new Sequelize.ENUM('hat', 'cat'); - if (['postgres', 'db2'].includes(dialect)) { + if (['postgres', 'oracle', 'db2'].includes(dialect)) { await testSuccess(Type, 'hat'); } else { testFailure(Type); @@ -462,7 +514,7 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { }); } - if (['postgres', 'sqlite'].includes(dialect)) { + if (['postgres', 'sqlite', 'oracle'].includes(dialect)) { // postgres actively supports IEEE floating point literals, and sqlite doesn't care what we throw at it it('should store and parse IEEE floating point literals (NaN and Infinity)', async function() { const Model = this.sequelize.define('model', { diff --git a/test/integration/error.test.js b/test/integration/error.test.js index beba2d3c9583..96821a3c6b95 100644 --- a/test/integration/error.test.js +++ b/test/integration/error.test.js @@ -374,7 +374,7 @@ describe(Support.getTestDialectTeaser('Sequelize Errors'), () => { await expect(User.create({ name: 'jan' })).to.be.rejectedWith(Sequelize.UniqueConstraintError); // And when the model is not passed at all - if (dialect === 'db2') { + if (dialect === 'db2' || dialect === 'oracle') { await expect(this.sequelize.query('INSERT INTO "users" ("name") VALUES (\'jan\')')).to.be.rejectedWith(Sequelize.UniqueConstraintError); } else { await expect(this.sequelize.query('INSERT INTO users (name) VALUES (\'jan\')')).to.be.rejectedWith(Sequelize.UniqueConstraintError); diff --git a/test/integration/include.test.js b/test/integration/include.test.js index ead06dbb2362..298b9ec18462 100644 --- a/test/integration/include.test.js +++ b/test/integration/include.test.js @@ -656,6 +656,11 @@ describe(Support.getTestDialectTeaser('Include'), () => { Sequelize.literal('EXISTS(SELECT 1 FROM SYSIBM.SYSDUMMY1) AS "PostComments.someProperty"'), [Sequelize.literal('EXISTS(SELECT 1 FROM SYSIBM.SYSDUMMY1)'), 'someProperty2'] ]; + } else if (dialect === 'oracle') { + findAttributes = [ + Sequelize.literal('(CASE WHEN EXISTS(SELECT 1 FROM DUAL) THEN 1 ELSE 0 END) AS "PostComments.someProperty"'), + [Sequelize.literal('(CASE WHEN EXISTS(SELECT 1 FROM DUAL) THEN 1 ELSE 0 END)'), 'someProperty2'] + ]; } else { findAttributes = [ Sequelize.literal('EXISTS(SELECT 1) AS "PostComments.someProperty"'), diff --git a/test/integration/include/findAll.test.js b/test/integration/include/findAll.test.js index 29d9e062a24f..679c039c72ad 100644 --- a/test/integration/include/findAll.test.js +++ b/test/integration/include/findAll.test.js @@ -6,6 +6,7 @@ const chai = require('chai'), expect = chai.expect, Support = require('../support'), DataTypes = require('sequelize/lib/data-types'), + dialect = Support.getTestDialect(), _ = require('lodash'), promiseProps = require('p-props'); @@ -292,7 +293,8 @@ describe(Support.getTestDialectTeaser('Include'), () => { }); }); - it('should support an include with multiple different association types', async function() { + // On update cascade not supported in the Oracle dialect + (dialect !== 'oracle' ? it : it.skip)('should support an include with multiple different association types', async function() { const User = this.sequelize.define('User', {}), Product = this.sequelize.define('Product', { title: DataTypes.STRING @@ -1136,7 +1138,8 @@ describe(Support.getTestDialectTeaser('Include'), () => { expect(products[0].Tags.length).to.equal(1); }); - it('should be possible to extend the on clause with a where option on nested includes', async function() { + // On update cascade not supported in the Oracle dialect + (dialect !== 'oracle' ? it : it.skip)('should be possible to extend the on clause with a where option on nested includes', async function() { const User = this.sequelize.define('User', { name: DataTypes.STRING }), @@ -1306,7 +1309,8 @@ describe(Support.getTestDialectTeaser('Include'), () => { }); }); - it('should be possible use limit, attributes and a where on a belongsTo with additional hasMany includes', async function() { + // On update cascade not supported in the Oracle dialect + (dialect !== 'oracle' ? it : it.skip)('should be possible use limit, attributes and a where on a belongsTo with additional hasMany includes', async function() { await this.fixtureA(); const products = await this.models.Product.findAll({ @@ -1522,7 +1526,8 @@ describe(Support.getTestDialectTeaser('Include'), () => { }); }); - it('should be possible to use limit and a where on a hasMany with a through model with additional includes', async function() { + // On update cascade not supported in the Oracle dialect + (dialect !== 'oracle' ? it : it.skip)('should be possible to use limit and a where on a hasMany with a through model with additional includes', async function() { await this.fixtureA(); const products = await this.models.Product.findAll({ diff --git a/test/integration/include/schema.test.js b/test/integration/include/schema.test.js index 7db23df577d0..d18c6a4a1060 100644 --- a/test/integration/include/schema.test.js +++ b/test/integration/include/schema.test.js @@ -179,7 +179,8 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { await this.sequelize.createSchema('account'); }); - it('should support an include with multiple different association types', async function() { + // On update cascade not supported in the Oracle dialect + (dialect !== 'oracle' ? it : it.skip)('should support an include with multiple different association types', async function() { await this.sequelize.dropSchema('account'); await this.sequelize.createSchema('account'); const AccUser = this.sequelize.define('AccUser', {}, { schema: 'account' }), @@ -867,7 +868,8 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { expect(products[0].Tags.length).to.equal(1); }); - it('should be possible to extend the on clause with a where option on nested includes', async function() { + // On update cascade not supported in the Oracle dialect + (dialect !== 'oracle' ? it : it.skip)('should be possible to extend the on clause with a where option on nested includes', async function() { const User = this.sequelize.define('User', { name: DataTypes.STRING }, { schema: 'account' }), @@ -1036,7 +1038,8 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { }); }); - it('should be possible use limit, attributes and a where on a belongsTo with additional hasMany includes', async function() { + // On update cascade not supported in the Oracle dialect + (dialect !== 'oracle' ? it : it.skip)('should be possible use limit, attributes and a where on a belongsTo with additional hasMany includes', async function() { await this.fixtureA(); const products = await this.models.Product.findAll({ @@ -1061,7 +1064,8 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { }); }); - it('should be possible to use limit and a where on a hasMany with additional includes', async function() { + // On update cascade not supported in the Oracle dialect + (dialect !== 'oracle' ? it : it.skip)('should be possible to use limit and a where on a hasMany with additional includes', async function() { await this.fixtureA(); const products = await this.models.Product.findAll({ @@ -1090,7 +1094,8 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { }); }); - it('should be possible to use limit and a where on a hasMany with a through model with additional includes', async function() { + // On update cascade not supported in the Oracle dialect + (dialect !== 'oracle' ? it : it.skip)('should be possible to use limit and a where on a hasMany with a through model with additional includes', async function() { await this.fixtureA(); const products = await this.models.Product.findAll({ diff --git a/test/integration/instance/values.test.js b/test/integration/instance/values.test.js index 14f24151adc0..402cfaf68045 100644 --- a/test/integration/instance/values.test.js +++ b/test/integration/instance/values.test.js @@ -122,6 +122,8 @@ describe(Support.getTestDialectTeaser('DAO'), () => { let now = dialect === 'sqlite' ? this.sequelize.fn('', this.sequelize.fn('datetime', 'now')) : this.sequelize.fn('NOW'); if (dialect === 'mssql') { now = this.sequelize.fn('', this.sequelize.fn('getdate')); + } else if (dialect === 'oracle') { + now = this.sequelize.fn('', this.sequelize.literal('SYSDATE')); } user.set({ d: now, diff --git a/test/integration/json.test.js b/test/integration/json.test.js index 37a511e5d4db..7e7de96bea10 100644 --- a/test/integration/json.test.js +++ b/test/integration/json.test.js @@ -26,7 +26,8 @@ describe('model', () => { it('should tell me that a column is json', async function() { const table = await this.sequelize.queryInterface.describeTable('Users'); // expected for mariadb 10.4 : https://jira.mariadb.org/browse/MDEV-15558 - if (dialect !== 'mariadb') { + // oracledb 19c doesn't support JSON and the DB datatype is BLOB + if (dialect !== 'mariadb' && dialect !== 'oracle') { expect(table.emergency_contact.type).to.equal('JSON'); } }); @@ -40,6 +41,8 @@ describe('model', () => { logging: sql => { if (dialect.match(/^mysql|mariadb/)) { expect(sql).to.include('?'); + } else if (dialect === 'oracle') { + expect(sql).to.include(':1'); } else { expect(sql).to.include('$1'); } diff --git a/test/integration/model.test.js b/test/integration/model.test.js index 3781030c697e..5ea60480d4e2 100644 --- a/test/integration/model.test.js +++ b/test/integration/model.test.js @@ -233,7 +233,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(user.deletedAtThisTime).to.exist; }); - it('returns proper defaultValues after save when setter is set', async function() { + // The Oracle dialect doesn't support empty string in a non-null column + (dialect !== 'oracle' ? it : it.skip)('returns proper defaultValues after save when setter is set', async function() { const titleSetter = sinon.spy(), Task = this.sequelize.define('TaskBuild', { title: { @@ -540,6 +541,24 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(idx3.fields).to.deep.equal([ { attribute: 'fieldD', length: undefined, order: undefined, collate: undefined } ]); + } else if (dialect === 'oracle') { + primary = args[0]; + idx1 = args[1]; + idx2 = args[2]; + idx3 = args[3]; + + expect(idx1.fields).to.deep.equal([ + { attribute: 'fieldB', length: undefined, order: 'ASC', collate: undefined }, + { attribute: 'fieldA', length: undefined, order: 'ASC', collate: undefined } + ]); + + expect(idx2.fields).to.deep.equal([ + { attribute: 'fieldC', length: undefined, order: 'ASC', collate: undefined } + ]); + + expect(idx3.fields).to.deep.equal([ + { attribute: 'fieldD', length: undefined, order: 'ASC', collate: undefined } + ]); } else { // And finally mysql returns the primary first, and then the rest in the order they were defined primary = args[0]; @@ -989,7 +1008,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { fields: ['secretValue'], logging(sql) { test = true; - if (dialect === 'mssql') { + if (dialect === 'mssql' || dialect === 'oracle') { expect(sql).to.not.contain('createdAt'); } else { expect(sql).to.match(/UPDATE\s+[`"]+User1s[`"]+\s+SET\s+[`"]+secretValue[`"]=(\$1|\?),[`"]+updatedAt[`"]+=(\$2|\?)\s+WHERE [`"]+id[`"]+\s=\s(\$3|\?)/); @@ -1627,15 +1646,15 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(await User.findOne({ where: { username: 'Bob' } })).to.be.null; const tobi = await User.findOne({ where: { username: 'Tobi' } }); await tobi.destroy(); - let sql = dialect === 'db2' ? 'SELECT * FROM "paranoidusers" WHERE "username"=\'Tobi\'' : 'SELECT * FROM paranoidusers WHERE username=\'Tobi\''; + let sql = dialect === 'db2' || dialect === 'oracle' ? 'SELECT * FROM "paranoidusers" WHERE "username"=\'Tobi\'' : 'SELECT * FROM paranoidusers WHERE username=\'Tobi\''; let result = await this.sequelize.query(sql, { plain: true }); expect(result.username).to.equal('Tobi'); await User.destroy({ where: { username: 'Tony' } }); - sql = dialect === 'db2' ? 'SELECT * FROM "paranoidusers" WHERE "username"=\'Tony\'' : 'SELECT * FROM paranoidusers WHERE username=\'Tony\''; + sql = dialect === 'db2' || dialect === 'oracle' ? 'SELECT * FROM "paranoidusers" WHERE "username"=\'Tony\'' : 'SELECT * FROM paranoidusers WHERE username=\'Tony\''; result = await this.sequelize.query(sql, { plain: true }); expect(result.username).to.equal('Tony'); await User.destroy({ where: { username: ['Tony', 'Max'] }, force: true }); - sql = dialect === 'db2' ? 'SELECT * FROM "paranoidusers"' : 'SELECT * FROM paranoidusers'; + sql = dialect === 'db2' || dialect === 'oracle' ? 'SELECT * FROM "paranoidusers"' : 'SELECT * FROM paranoidusers'; const [users] = await this.sequelize.query(sql, { raw: true }); expect(users).to.have.length(1); expect(users[0].username).to.equal('Tobi'); @@ -1813,7 +1832,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(count.find(i => i.data === 'B')).to.deep.equal({ data: 'B', count: 1 }); }); - if (dialect !== 'mssql' && dialect !== 'db2') { + if (dialect !== 'mssql' && dialect !== 'db2' && dialect !== 'oracle') { describe('aggregate', () => { it('allows grouping by aliased attribute', async function() { await this.User.aggregate('id', 'count', { @@ -2100,6 +2119,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { db2: 10, mariadb: 3, mysql: 1, + oracle: 2, sqlite: 1 }; expect(schemas).to.have.length(expectedLengths[dialect]); @@ -2150,7 +2170,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { test++; expect(sql).to.not.contain('special'); } - else if (['mysql', 'mssql', 'mariadb', 'db2'].includes(dialect)) { + else if (['mysql', 'mssql', 'mariadb', 'db2', 'oracle'].includes(dialect)) { test++; expect(sql).to.not.contain('special'); } @@ -2169,7 +2189,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { test++; expect(sql).to.contain('special'); } - else if (['mysql', 'mssql', 'mariadb', 'db2'].includes(dialect)) { + else if (['mysql', 'mssql', 'mariadb', 'db2', 'oracle'].includes(dialect)) { test++; expect(sql).to.contain('special'); } @@ -2195,7 +2215,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { UserPub.hasMany(ItemPub, { foreignKeyConstraint: true }); - if (['postgres', 'mssql', 'db2', 'mariadb'].includes(dialect)) { + if (['postgres', 'mssql', 'db2', 'mariadb', 'oracle'].includes(dialect)) { await Support.dropTestSchemas(this.sequelize); await this.sequelize.queryInterface.createSchema('prefix'); } @@ -2211,6 +2231,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(sql).to.match(/REFERENCES\s+"prefix"\."UserPubs" \("id"\)/); } else if (dialect === 'mssql') { expect(sql).to.match(/REFERENCES\s+\[prefix\]\.\[UserPubs\] \(\[id\]\)/); + } else if (dialect === 'oracle') { + expect(sql).to.match(/REFERENCES\s+"prefix"."UserPubs" \("id"\)/); } else if (dialect === 'mariadb') { expect(sql).to.match(/REFERENCES\s+`prefix`\.`UserPubs` \(`id`\)/); } else { @@ -2241,6 +2263,9 @@ describe(Support.getTestDialectTeaser('Model'), () => { } else if (dialect === 'mariadb') { expect(this.UserSpecialSync.getTableName().toString()).to.equal('`special`.`UserSpecials`'); expect(UserPublic.indexOf('INSERT INTO `UserPublics`')).to.be.above(-1); + } else if (dialect === 'oracle') { + expect(this.UserSpecialSync.getTableName().toString()).to.equal('"special"."UserSpecials"'); + expect(UserPublic.indexOf('INSERT INTO "UserPublics"')).to.be.above(-1); } else { expect(this.UserSpecialSync.getTableName().toString()).to.equal('`special.UserSpecials`'); expect(UserPublic).to.include('INSERT INTO `UserPublics`'); @@ -2257,6 +2282,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(UserSpecial).to.include('INSERT INTO `special.UserSpecials`'); } else if (dialect === 'mssql') { expect(UserSpecial).to.include('INSERT INTO [special].[UserSpecials]'); + } else if (dialect === 'oracle') { + expect(UserSpecial).to.include('INSERT INTO "special"."UserSpecials"'); } else if (dialect === 'mariadb') { expect(UserSpecial).to.include('INSERT INTO `special`.`UserSpecials`'); } else { @@ -2272,6 +2299,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(user).to.include('UPDATE "special"."UserSpecials"'); } else if (dialect === 'mssql') { expect(user).to.include('UPDATE [special].[UserSpecials]'); + } else if (dialect === 'oracle') { + expect(user).to.include('UPDATE "special"."UserSpecials"'); } else if (dialect === 'mariadb') { expect(user).to.include('UPDATE `special`.`UserSpecials`'); } else { @@ -2373,6 +2402,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(err.message).to.match(/Could not create constraint/); } else if (dialect === 'db2') { expect(err.message).to.match(/ is an undefined name/); + } else if (dialect === 'oracle') { + expect(err.message).to.match(/ORA-00942: table or view does not exist/); } else { throw new Error('Undefined dialect!'); } diff --git a/test/integration/model/attributes/field.test.js b/test/integration/model/attributes/field.test.js index 15a0f9afca8d..fb9221a01a47 100644 --- a/test/integration/model/attributes/field.test.js +++ b/test/integration/model/attributes/field.test.js @@ -468,6 +468,11 @@ describe(Support.getTestDialectTeaser('Model'), () => { Sequelize.literal('1 AS "someProperty"'), [Sequelize.literal('1'), 'someProperty2'] ]; + } else if (dialect === 'oracle') { + findAttributes = [ + Sequelize.literal('(CASE WHEN EXISTS(SELECT 1 FROM DUAL) THEN 1 ELSE 0 END) AS "someProperty"'), + [Sequelize.literal('(CASE WHEN EXISTS(SELECT 1 FROM DUAL) THEN 1 ELSE 0 END)'), 'someProperty2'] + ]; } else { findAttributes = [ Sequelize.literal('EXISTS(SELECT 1) AS "someProperty"'), diff --git a/test/integration/model/attributes/types.test.js b/test/integration/model/attributes/types.test.js index 4ee1dc16774f..7267dbbd71a2 100644 --- a/test/integration/model/attributes/types.test.js +++ b/test/integration/model/attributes/types.test.js @@ -103,6 +103,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { boolQuery = 'CAST(CASE WHEN EXISTS(SELECT 1) THEN 1 ELSE 0 END AS BIT) AS "someBoolean"'; } else if (dialect === 'db2') { boolQuery = '1 AS "someBoolean"'; + } else if (dialect === 'oracle') { + boolQuery = '(CASE WHEN EXISTS(SELECT 1 FROM DUAL) THEN 1 ELSE 0 END) AS "someBoolean"'; } const post = await Post.findOne({ attributes: ['id', 'text', Sequelize.literal(boolQuery)] }); diff --git a/test/integration/model/bulk-create.test.js b/test/integration/model/bulk-create.test.js index 4a40ca992312..633c1e8bd822 100644 --- a/test/integration/model/bulk-create.test.js +++ b/test/integration/model/bulk-create.test.js @@ -158,7 +158,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { style: 'ipa' }], { logging(sql) { - if (dialect === 'postgres') { + if (dialect === 'postgres' || dialect === 'oracle') { expect(sql).to.include('INSERT INTO "Beers" ("id","style","createdAt","updatedAt") VALUES (DEFAULT'); } else if (dialect === 'db2') { expect(sql).to.include('INSERT INTO "Beers" ("style","createdAt","updatedAt") VALUES'); diff --git a/test/integration/model/create.test.js b/test/integration/model/create.test.js index 137812185a2d..d559c9a445fa 100644 --- a/test/integration/model/create.test.js +++ b/test/integration/model/create.test.js @@ -450,7 +450,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { }); } - (dialect !== 'sqlite' && dialect !== 'mssql' && dialect !== 'db2' ? it : it.skip)('should not fail silently with concurrency higher than pool, a unique constraint and a create hook resulting in mismatched values', async function() { + (dialect !== 'sqlite' && dialect !== 'mssql' && dialect !== 'oracle' && dialect !== 'db2' ? it : it.skip)('should not fail silently with concurrency higher than pool, a unique constraint and a create hook resulting in mismatched values', async function() { const User = this.sequelize.define('user', { username: { type: DataTypes.STRING, diff --git a/test/integration/model/findAll/order.test.js b/test/integration/model/findAll/order.test.js index 6f48a73e57db..adddecdcc169 100644 --- a/test/integration/model/findAll/order.test.js +++ b/test/integration/model/findAll/order.test.js @@ -22,7 +22,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { }); }); - if (current.dialect.name !== 'mssql') { + if (current.dialect.name !== 'mssql' && current.dialect.name !== 'oracle') { const email = current.dialect.name === 'db2' ? '"email"' : 'email'; it('should work with order: literal()', async function() { const users = await this.User.findAll({ @@ -86,7 +86,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { } it('should not throw on a literal', async function() { - if (current.dialect.name === 'db2') { + if (current.dialect.name === 'db2' || current.dialect.name === 'oracle') { await this.User.findAll({ order: [ ['id', this.sequelize.literal('ASC, "name" DESC')] diff --git a/test/integration/pool.test.js b/test/integration/pool.test.js index 98357e7cc679..76bf6a380000 100644 --- a/test/integration/pool.test.js +++ b/test/integration/pool.test.js @@ -14,6 +14,10 @@ function assertSameConnection(newConnection, oldConnection) { expect(oldConnection.processID).to.be.equal(newConnection.processID).and.to.be.ok; break; + case 'oracle': + expect(oldConnection).to.be.equal(newConnection); + break; + case 'mariadb': case 'mysql': expect(oldConnection.threadId).to.be.equal(newConnection.threadId).and.to.be.ok; @@ -48,6 +52,10 @@ function assertNewConnection(newConnection, oldConnection) { expect(oldConnection.connected).to.not.be.ok; break; + case 'oracle': + expect(oldConnection).to.not.be.equal(newConnection); + break; + case 'mssql': // Flaky test expect(newConnection.dummyId).to.not.be.ok; @@ -110,7 +118,7 @@ describe(Support.getTestDialectTeaser('Pooling'), () => { }); it('should obtain new connection when released connection dies inside pool', async () => { - function simulateUnexpectedError(connection) { + async function simulateUnexpectedError(connection) { // should never be returned again if (dialect === 'mssql') { attachMSSQLUniqueId(connection).close(); @@ -118,6 +126,9 @@ describe(Support.getTestDialectTeaser('Pooling'), () => { connection.end(); } else if (dialect === 'db2') { connection.closeSync(); + } else if (dialect === 'oracle') { + // For the Oracle dialect close is an async function + await connection.close(); } else { connection.close(); } @@ -131,7 +142,7 @@ describe(Support.getTestDialectTeaser('Pooling'), () => { const oldConnection = await cm.getConnection(); await cm.releaseConnection(oldConnection); - simulateUnexpectedError(oldConnection); + await simulateUnexpectedError(oldConnection); const newConnection = await cm.getConnection(); assertNewConnection(newConnection, oldConnection); diff --git a/test/integration/query-interface.test.js b/test/integration/query-interface.test.js index d514d192a65a..932446804651 100644 --- a/test/integration/query-interface.test.js +++ b/test/integration/query-interface.test.js @@ -38,13 +38,28 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { async function cleanup(sequelize) { if (dialect === 'db2') { await sequelize.query('DROP VIEW V_Fail'); + } else if (dialect === 'oracle') { + const plsql = [ + 'DECLARE', + ' V_COUNT INTEGER;', + 'BEGIN', + ' V_COUNT := 0;', + ' SELECT COUNT(1) INTO V_COUNT FROM USER_VIEWS WHERE VIEW_NAME = \'V_FAIL\';', + ' IF V_COUNT != 0 THEN', + ' EXECUTE IMMEDIATE ', + '\'DROP VIEW V_Fail\'', + ';', + ' END IF;', + 'END;' + ].join(''); + await sequelize.query(plsql); } else { await sequelize.query('DROP VIEW IF EXISTS V_Fail'); } } await this.queryInterface.createTable('my_test_table', { name: DataTypes.STRING }); await cleanup(this.sequelize); - const sql = dialect === 'db2' ? 'CREATE VIEW V_Fail AS SELECT 1 Id FROM SYSIBM.SYSDUMMY1' : 'CREATE VIEW V_Fail AS SELECT 1 Id'; + const sql = dialect === 'db2' ? 'CREATE VIEW V_Fail AS SELECT 1 Id FROM SYSIBM.SYSDUMMY1' : `CREATE VIEW V_Fail AS SELECT 1 Id${ Support.addDualInSelect()}`; await this.sequelize.query(sql); let tableNames = await this.queryInterface.showAllTables(); await cleanup(this.sequelize); @@ -54,7 +69,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { expect(tableNames).to.deep.equal(['my_test_table']); }); - if (dialect !== 'sqlite' && dialect !== 'postgres' && dialect !== 'db2') { + if (dialect !== 'sqlite' && dialect !== 'postgres' && dialect !== 'db2' && dialect != 'oracle') { // NOTE: sqlite doesn't allow querying between databases and // postgres requires creating a new connection to create a new table. it('should not show tables in other databases', async function() { @@ -101,7 +116,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { }); await this.queryInterface.renameTable('my_test_table', 'my_test_table_new'); let tableNames = await this.queryInterface.showAllTables(); - if (['mssql', 'mariadb', 'db2'].includes(dialect)) { + if (['mssql', 'mariadb', 'db2', 'oracle'].includes(dialect)) { tableNames = tableNames.map(v => v.tableName); } expect(tableNames).to.contain('my_test_table_new'); @@ -143,7 +158,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { }); await this.queryInterface.dropAllTables({ skip: ['skipme'] }); let tableNames = await this.queryInterface.showAllTables(); - if (['mssql', 'mariadb', 'db2'].includes(dialect)) { + if (['mssql', 'mariadb', 'db2', 'oracle'].includes(dialect)) { tableNames = tableNames.map(v => v.tableName); } expect(tableNames).to.contain('skipme'); @@ -463,6 +478,8 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { expect(Object.keys(foreignKeys[0])).to.have.length(8); } else if (['mysql', 'mariadb', 'mssql'].includes(dialect)) { expect(Object.keys(foreignKeys[0])).to.have.length(12); + } else if (dialect === 'oracle') { + expect(Object.keys(foreignKeys[0])).to.have.length(6); } else { throw new Error(`This test doesn't support ${dialect}`); } @@ -631,7 +648,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { field: 'username' }, onDelete: 'cascade', - onUpdate: 'cascade', + onUpdate: dialect !== 'oracle' ? 'cascade' : null, type: 'foreign key' }); let constraints = await this.queryInterface.showConstraint('posts'); @@ -653,8 +670,11 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { throw new Error('Error not thrown...'); } catch (error) { expect(error).to.be.instanceOf(Sequelize.UnknownConstraintError); - expect(error.table).to.equal('users'); - expect(error.constraint).to.equal('unknown__constraint__name'); + // The Oracle dialect, error messages doesn't have table and constraint information + if (dialect != 'oracle') { + expect(error.table).to.equal('users'); + expect(error.constraint).to.equal('unknown__constraint__name'); + } } }); }); diff --git a/test/integration/query-interface/changeColumn.test.js b/test/integration/query-interface/changeColumn.test.js index a1e2501d9370..69bd4f8df114 100644 --- a/test/integration/query-interface/changeColumn.test.js +++ b/test/integration/query-interface/changeColumn.test.js @@ -48,6 +48,8 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { expect(table.currency.type).to.equal('DOUBLE PRECISION'); } else if (dialect === 'db2') { expect(table.currency.type).to.equal('DOUBLE'); + } else if (dialect === 'oracle') { + expect(table.currency.type).to.equal('BINARY_FLOAT'); } else { expect(table.currency.type).to.equal('FLOAT'); } @@ -82,6 +84,8 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { expect(table.currency.type).to.equal('DOUBLE PRECISION'); } else if (dialect === 'db2') { expect(table.currency.type).to.equal('DOUBLE'); + } else if (dialect === 'oracle') { + expect(table.currency.type).to.equal('BINARY_FLOAT'); } else { expect(table.currency.type).to.equal('FLOAT'); } @@ -223,7 +227,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { expect(describedTable.level_id.allowNull).to.not.equal(firstTable.level_id.allowNull); expect(describedTable.level_id.allowNull).to.be.equal(true); }); - if (dialect !== 'db2') { + if (dialect !== 'db2' && dialect !== 'oracle') { it('should change the comment of column', async function() { const describedTable = await this.queryInterface.describeTable({ tableName: 'users' diff --git a/test/integration/query-interface/createTable.test.js b/test/integration/query-interface/createTable.test.js index b8dd42e19bd6..b9133e2ebe45 100644 --- a/test/integration/query-interface/createTable.test.js +++ b/test/integration/query-interface/createTable.test.js @@ -77,7 +77,8 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { break; case 'mariadb': case 'mysql': - case 'db2': + case 'db2': + case 'oracle': // name + email expect(indexes[1].unique).to.be.true; expect(indexes[1].fields[0].attribute).to.equal('name'); diff --git a/test/integration/query-interface/describeTable.test.js b/test/integration/query-interface/describeTable.test.js index 796ce1e10eaa..5c21362bee34 100644 --- a/test/integration/query-interface/describeTable.test.js +++ b/test/integration/query-interface/describeTable.test.js @@ -73,6 +73,9 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { let assertVal = 'VARCHAR(255)'; switch (dialect) { + case 'oracle': + assertVal = 'NVARCHAR2'; + break; case 'postgres': assertVal = 'CHARACTER VARYING(255)'; break; @@ -87,6 +90,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { expect(username.allowNull).to.be.true; switch (dialect) { + case 'oracle': case 'sqlite': expect(username.defaultValue).to.be.undefined; break; @@ -102,6 +106,9 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { assertVal = 'TINYINT(1)'; switch (dialect) { + case 'oracle': + assertVal = 'CHAR'; + break; case 'postgres': case 'db2': assertVal = 'BOOLEAN'; @@ -113,6 +120,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { expect(isAdmin.type).to.equal(assertVal); expect(isAdmin.allowNull).to.be.true; switch (dialect) { + case 'oracle': case 'sqlite': expect(isAdmin.defaultValue).to.be.undefined; break; diff --git a/test/integration/sequelize.test.js b/test/integration/sequelize.test.js index 59361493ecd6..e2309b0460e1 100644 --- a/test/integration/sequelize.test.js +++ b/test/integration/sequelize.test.js @@ -12,7 +12,7 @@ const sinon = require('sinon'); const current = Support.sequelize; const qq = str => { - if (['postgres', 'mssql', 'db2'].includes(dialect)) { + if (['postgres', 'mssql', 'db2', 'oracle'].includes(dialect)) { return `"${str}"`; } if (['mysql', 'mariadb', 'sqlite'].includes(dialect)) { @@ -123,13 +123,14 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { .sequelizeWithInvalidConnection .authenticate(); } catch (err) { - console.log(err); expect( err.message.includes('connect ECONNREFUSED') || err.message.includes('invalid port number') || err.message.match(/should be >=? 0 and < 65536/) || err.message.includes('Login failed for user') || err.message.includes('A communication error has been detected') || + err.message.includes('ORA-12545') || + err.message.includes('ORA-12541') || err.message.includes('must be > 0 and < 65536') ).to.be.ok; } @@ -362,7 +363,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { const Photo = this.sequelize.define('Foto', { name: DataTypes.STRING }, { tableName: 'photos' }); await Photo.sync({ force: true }); let tableNames = await this.sequelize.getQueryInterface().showAllTables(); - if (['mssql', 'mariadb', 'db2'].includes(dialect)) { + if (['mssql', 'mariadb', 'db2', 'oracle'].includes(dialect)) { tableNames = tableNames.map(v => v.tableName); } expect(tableNames).to.include('photos'); @@ -452,6 +453,8 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { expect(err.message).to.equal('Login failed for user \'bar\'.'); } else if (dialect === 'db2') { expect(err.message).to.include('A communication error has been detected'); + } else if (dialect === 'oracle') { + expect(err.message).to.include('NJS-007'); } else { expect(err.message.toString()).to.match(/.*Access denied.*/); } diff --git a/test/integration/sequelize.transaction.test.js b/test/integration/sequelize.transaction.test.js index 35ed328622b6..f6fef21a3a48 100644 --- a/test/integration/sequelize.transaction.test.js +++ b/test/integration/sequelize.transaction.test.js @@ -107,6 +107,9 @@ describe(Support.getTestDialectTeaser('Sequelize#transaction'), () => { case 'mssql': query = 'WAITFOR DELAY \'00:00:02\';'; break; + case 'oracle': + query = 'BEGIN DBMS_SESSION.sleep(2); END;'; + break; default: break; } diff --git a/test/integration/sequelize/query.test.js b/test/integration/sequelize/query.test.js index 767940b5dbe9..0063eaec886a 100644 --- a/test/integration/sequelize/query.test.js +++ b/test/integration/sequelize/query.test.js @@ -10,7 +10,7 @@ const moment = require('moment'); const { DatabaseError, UniqueConstraintError, ForeignKeyConstraintError } = Support.Sequelize; const qq = str => { - if (['postgres', 'mssql', 'db2'].includes(dialect)) { + if (['postgres', 'mssql', 'db2', 'oracle'].includes(dialect)) { return `"${str}"`; } if (['mysql', 'mariadb', 'sqlite'].includes(dialect)) { @@ -19,6 +19,13 @@ const qq = str => { return str; }; +const dateLiteral = str => { + if (dialect === 'oracle') { + return `to_date('${str}','YYYY-MM-DD HH24:MI:SS')`; + } + return `'${str}'`; +}; + describe(Support.getTestDialectTeaser('Sequelize'), () => { describe('query', () => { afterEach(function() { @@ -38,9 +45,9 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { } }); - this.insertQuery = `INSERT INTO ${qq(this.User.tableName)} (username, email_address, ${ + this.insertQuery = `INSERT INTO ${qq(this.User.tableName)} (${qq('username')}, ${qq('email_address')}, ${ qq('createdAt') }, ${qq('updatedAt') - }) VALUES ('john', 'john@gmail.com', '2012-01-01 10:10:10', '2012-01-01 10:10:10')`; + }) VALUES ('john', 'john@gmail.com',${dateLiteral('2012-01-01 10:10:10')},${dateLiteral('2012-01-01 10:10:10')})`; if (dialect === 'db2') { this.insertQuery = `INSERT INTO ${qq(this.User.tableName)} ("username", "email_address", ${ qq('createdAt') }, ${qq('updatedAt')}) VALUES ('john', 'john@gmail.com', '2012-01-01 10:10:10', '2012-01-01 10:10:10')`; @@ -56,7 +63,9 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { await this.sequelize.query(this.insertQuery); }); - it('executes a query if a placeholder value is an array', async function() { + // Oracle dialect doesn't support insert of multiple rows using insert into statement + // INSERT ALL INTO statement can be used instead + (dialect !== 'oracle' ? it : it.skip)('executes a query if a placeholder value is an array', async function() { await this.sequelize.query(`INSERT INTO ${qq(this.User.tableName)} (${qq('username')}, ${qq('email_address')}, ` + `${qq('createdAt')}, ${qq('updatedAt')}) VALUES ?;`, { replacements: [[ @@ -93,8 +102,8 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { it('properly bind parameters on extra retries', async function() { const payload = { username: 'test', - createdAt: '2010-10-10 00:00:00', - updatedAt: '2010-10-10 00:00:00' + createdAt: dialect === 'oracle' ? new Date('2010-10-10 00:00:00') : '2010-10-10 00:00:00', + updatedAt: dialect === 'oracle' ? new Date('2010-10-10 00:00:00') : '2010-10-10 00:00:00' }; const spy = sinon.spy(); @@ -125,7 +134,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { benchmark: true }); - await sequelize.query('select 1;'); + await sequelize.query(`select 1${Support.addDualInSelect()};`); expect(logger.calledOnce).to.be.true; expect(logger.args[0][0]).to.be.match(/Executed \((\d*|default)\): select 1/); expect(typeof logger.args[0][1] === 'number').to.be.true; @@ -134,7 +143,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { it('executes a query with benchmarking option and custom logger', async function() { const logger = sinon.spy(); - await this.sequelize.query('select 1;', { + await this.sequelize.query(`select 1${Support.addDualInSelect()};`, { logging: logger, benchmark: true }); @@ -199,7 +208,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { if (dialect === 'db2') { typeCast = '::VARCHAR'; } - await this.sequelize.query(`select $1${typeCast} as foo, $2${typeCast} as bar`, { bind: ['foo', 'bar'], logging: s=>logSql = s }); + await this.sequelize.query(`select $1${typeCast} as foo, $2${typeCast} as bar${Support.addDualInSelect()}`, { bind: ['foo', 'bar'], logging: s=>logSql = s }); expect(logSql).to.match(/; ("foo", "bar"|{"(\$1|0)":"foo","(\$2|1)":"bar"})/); }); }); @@ -372,7 +381,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { const query = `INSERT INTO ${qq(this.User.tableName)} (username, email_address, ${ qq('createdAt') }, ${qq('updatedAt') }) VALUES ('duplicate', 'duplicate@gmail.com', '2012-01-01 10:10:10', '2012-01-01 10:10:10')`; - if (dialect === 'db2') { + if (dialect === 'db2' || dialect === 'oracle') { this.query = `INSERT INTO ${qq(this.User.tableName)} ("username", "email_address", ${ qq('createdAt') }, ${qq('updatedAt') }) VALUES ('duplicate', 'duplicate@gmail.com', '2012-01-01 10:10:10', '2012-01-01 10:10:10')`; @@ -386,7 +395,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { } catch (err) { error = err; } - if (dialect === 'db2') { + if (dialect === 'db2' || dialect === 'oracle') { expect(error).to.be.instanceOf(DatabaseError); } else { expect(error).to.be.instanceOf(UniqueConstraintError); @@ -403,7 +412,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { )}, ${qq( 'updatedAt' )}) VALUES (123456789, '2012-01-01 10:10:10', '2012-01-01 10:10:10', '2012-01-01 10:10:10')`; - if (dialect === 'db2') { + if (dialect === 'db2' || dialect === 'oracle') { this.query = `INSERT INTO ${qq(this.UserVisit.tableName)} ("user_id", "visited_at", ${qq( 'createdAt' )}, ${qq( @@ -414,7 +423,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { } catch (err) { error = err; } - if (dialect === 'db2') { + if (dialect === 'db2' || dialect === 'oracle') { expect(error).to.be.instanceOf(DatabaseError); } else { expect(error).to.be.instanceOf(ForeignKeyConstraintError); @@ -545,8 +554,8 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { date.setMilliseconds(0); let sql = 'select ? as number, ? as date,? as string,? as boolean,? as buffer'; - if (dialect === 'db2') { - sql = 'select ? as "number", ? as "date",? as "string",? as "boolean",? as "buffer"'; + if (dialect === 'db2' || dialect === 'oracle') { + sql = `select ? as "number", ? as "date",? as "string",? as "boolean",? as "buffer"${ Support.addDualInSelect()}`; } const result = await this.sequelize.query({ query: sql, @@ -561,7 +570,11 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { const res = result[0] || {}; res.date = res.date && new Date(res.date); res.boolean = res.boolean && true; - if (typeof res.buffer === 'string' && res.buffer.startsWith('\\x')) { + + // For Oracle dialect BLOB data doesn't begin with \\x hence we need to convert whole buffer to hex type + if (typeof res.buffer === 'string' && dialect === 'oracle') { + res.buffer = Buffer.from(res.buffer, 'hex'); + } else if (typeof res.buffer === 'string' && res.buffer.startsWith('\\x')) { res.buffer = Buffer.from(res.buffer.substring(2), 'hex'); } expect(res).to.deep.equal({ @@ -581,8 +594,8 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { this.values = [1, 2]; } get query() { - if (dialect === 'db2') { - return 'select ? as "foo", ? as "bar"'; + if (dialect === 'db2' || dialect === 'oracle') { + return `select ? as "foo", ? as "bar"${ Support.addDualInSelect()}`; } return 'select ? as foo, ? as bar'; } @@ -592,10 +605,10 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { expect(logSql).to.not.include('?'); }); - const expected = dialect === 'db2' ? [{ FOO: 1, BAR: 2 }] : [{ foo: 1, bar: 2 }]; + const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: 2 }] : [{ foo: 1, bar: 2 }]; it('uses properties `query` and `values` if query is tagged', async function() { let logSql; - const result = await this.sequelize.query({ query: 'select ? as foo, ? as bar', values: [1, 2] }, { type: this.sequelize.QueryTypes.SELECT, logging(s) { logSql = s; } }); + const result = await this.sequelize.query({ query: `select ? as foo, ? as bar${ Support.addDualInSelect()}`, values: [1, 2] }, { type: this.sequelize.QueryTypes.SELECT, logging(s) { logSql = s; } }); expect(result).to.deep.equal(expected); expect(logSql).to.not.include('?'); }); @@ -603,7 +616,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { it('uses properties `query` and `bind` if query is tagged', async function() { const typeCast = dialect === 'postgres' || dialect === 'db2' ? '::int' : ''; let logSql; - const result = await this.sequelize.query({ query: `select $1${typeCast} as foo, $2${typeCast} as bar`, bind: [1, 2] }, { type: this.sequelize.QueryTypes.SELECT, logging(s) { logSql = s; } }); + const result = await this.sequelize.query({ query: `select $1${typeCast} as foo, $2${typeCast} as bar${Support.addDualInSelect()}`, bind: [1, 2] }, { type: this.sequelize.QueryTypes.SELECT, logging(s) { logSql = s; } }); expect(result).to.deep.equal(expected); if (['postgres', 'sqlite'].includes(dialect)) { expect(logSql).to.include('$1'); @@ -617,52 +630,52 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { }); it('dot separated attributes when doing a raw query without nest', async function() { - const tickChar = ['postgres', 'mssql', 'db2'].includes(dialect) ? '"' : '`', - sql = `select 1 as ${Sequelize.Utils.addTicks('foo.bar.baz', tickChar)}`; + const tickChar = ['postgres', 'mssql', 'db2', 'oracle'].includes(dialect) ? '"' : '`', + sql = `select 1 as ${Sequelize.Utils.addTicks('foo.bar.baz', tickChar)}${Support.addDualInSelect()}`; await expect(this.sequelize.query(sql, { raw: true, nest: false }).then(obj => obj[0])).to.eventually.deep.equal([{ 'foo.bar.baz': 1 }]); }); it('destructs dot separated attributes when doing a raw query using nest', async function() { - const tickChar = ['postgres', 'mssql', 'db2'].includes(dialect) ? '"' : '`', - sql = `select 1 as ${Sequelize.Utils.addTicks('foo.bar.baz', tickChar)}`; + const tickChar = ['postgres', 'mssql', 'db2', 'oracle'].includes(dialect) ? '"' : '`', + sql = `select 1 as ${Sequelize.Utils.addTicks('foo.bar.baz', tickChar)}${Support.addDualInSelect()}`; const result = await this.sequelize.query(sql, { raw: true, nest: true }); expect(result).to.deep.equal([{ foo: { bar: { baz: 1 } } }]); }); it('replaces token with the passed array', async function() { - const result = await this.sequelize.query('select ? as foo, ? as bar', { type: this.sequelize.QueryTypes.SELECT, replacements: [1, 2] }); + const result = await this.sequelize.query(`select ? as foo, ? as bar${ Support.addDualInSelect()}`, { type: this.sequelize.QueryTypes.SELECT, replacements: [1, 2] }); expect(result).to.deep.equal(expected); }); it('replaces named parameters with the passed object', async function() { - await expect(this.sequelize.query('select :one as foo, :two as bar', { raw: true, replacements: { one: 1, two: 2 } }).then(obj => obj[0])) + await expect(this.sequelize.query(`select :one as foo, :two as bar${ Support.addDualInSelect()}`, { raw: true, replacements: { one: 1, two: 2 } }).then(obj => obj[0])) .to.eventually.deep.equal(expected); }); it('replaces named parameters with the passed object and ignore those which does not qualify', async function() { - const expected = dialect === 'db2' ? [{ FOO: 1, BAR: 2, BAZ: '00:00' }] : [{ foo: 1, bar: 2, baz: '00:00' }]; - await expect(this.sequelize.query('select :one as foo, :two as bar, \'00:00\' as baz', { raw: true, replacements: { one: 1, two: 2 } }).then(obj => obj[0])) + const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: 2, BAZ: '00:00' }] : [{ foo: 1, bar: 2, baz: '00:00' }]; + await expect(this.sequelize.query(`select :one as foo, :two as bar, '00:00' as baz${ Support.addDualInSelect()}`, { raw: true, replacements: { one: 1, two: 2 } }).then(obj => obj[0])) .to.eventually.deep.equal(expected); }); it('replaces named parameters with the passed object using the same key twice', async function() { - const expected = dialect === 'db2' ? [{ FOO: 1, BAR: 2, BAZ: 1 }] : [{ foo: 1, bar: 2, baz: 1 }]; - await expect(this.sequelize.query('select :one as foo, :two as bar, :one as baz', { raw: true, replacements: { one: 1, two: 2 } }).then(obj => obj[0])) + const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: 2, BAZ: 1 }] : [{ foo: 1, bar: 2, baz: 1 }]; + await expect(this.sequelize.query(`select :one as foo, :two as bar, :one as baz${ Support.addDualInSelect()}`, { raw: true, replacements: { one: 1, two: 2 } }).then(obj => obj[0])) .to.eventually.deep.equal(expected); }); it('replaces named parameters with the passed object having a null property', async function() { - const expected = dialect === 'db2' ? [{ FOO: 1, BAR: null }] : [{ foo: 1, bar: null }]; - await expect(this.sequelize.query('select :one as foo, :two as bar', { raw: true, replacements: { one: 1, two: null } }).then(obj => obj[0])) + const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: null }] : [{ foo: 1, bar: null }]; + await expect(this.sequelize.query(`select :one as foo, :two as bar${ Support.addDualInSelect()}`, { raw: true, replacements: { one: 1, two: null } }).then(obj => obj[0])) .to.eventually.deep.equal(expected); }); it('binds token with the passed array', async function() { const typeCast = dialect === 'postgres' || dialect === 'db2' ? '::int' : ''; let logSql; - const result = await this.sequelize.query(`select $1${typeCast} as foo, $2${typeCast} as bar`, { type: this.sequelize.QueryTypes.SELECT, bind: [1, 2], logging(s) { logSql = s;} }); + const result = await this.sequelize.query(`select $1${typeCast} as foo, $2${typeCast} as bar${Support.addDualInSelect()}`, { type: this.sequelize.QueryTypes.SELECT, bind: [1, 2], logging(s) { logSql = s;} }); expect(result).to.deep.equal(expected); if (['postgres', 'sqlite'].includes(dialect)) { expect(logSql).to.include('$1'); @@ -672,7 +685,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { it('binds named parameters with the passed object', async function() { const typeCast = dialect === 'postgres' || dialect === 'db2' ? '::int' : ''; let logSql; - const result = await this.sequelize.query(`select $one${typeCast} as foo, $two${typeCast} as bar`, { raw: true, bind: { one: 1, two: 2 }, logging(s) { logSql = s; } }); + const result = await this.sequelize.query(`select $one${typeCast} as foo, $two${typeCast} as bar${Support.addDualInSelect()}`, { raw: true, bind: { one: 1, two: 2 }, logging(s) { logSql = s; } }); expect(result[0]).to.deep.equal(expected); if (dialect === 'postgres') { expect(logSql).to.include('$1'); @@ -686,8 +699,9 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { it('binds named parameters with the passed object using the same key twice', async function() { const typeCast = dialect === 'postgres' ? '::int' : ''; let logSql; - const result = await this.sequelize.query(`select $one${typeCast} as foo, $two${typeCast} as bar, $one${typeCast} as baz`, { raw: true, bind: { one: 1, two: 2 }, logging(s) { logSql = s; } }); - expect(result[0]).to.deep.equal([{ foo: 1, bar: 2, baz: 1 }]); + const result = await this.sequelize.query(`select $one${typeCast} as foo, $two${typeCast} as bar, $one${typeCast} as baz${Support.addDualInSelect()}`, { raw: true, bind: { one: 1, two: 2 }, logging(s) { logSql = s; } }); + const expected = dialect !== 'oracle' ? [{ foo: 1, bar: 2, baz: 1 }] : [{ FOO: 1, BAR: 2, BAZ: 1 }]; + expect(result[0]).to.deep.equal(expected); if (dialect === 'postgres') { expect(logSql).to.include('$1'); expect(logSql).to.include('$2'); @@ -697,16 +711,16 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { } it('binds named parameters with the passed object having a null property', async function() { const typeCast = dialect === 'postgres' || dialect === 'db2' ? '::int' : ''; - const result = await this.sequelize.query(`select $one${typeCast} as foo, $two${typeCast} as bar`, { raw: true, bind: { one: 1, two: null } }); - const expected = dialect === 'db2' ? [{ FOO: 1, BAR: null }] : [{ foo: 1, bar: null }]; + const result = await this.sequelize.query(`select $one${typeCast} as foo, $two${typeCast} as bar${Support.addDualInSelect()}`, { raw: true, bind: { one: 1, two: null } }); + const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: null }] : [{ foo: 1, bar: null }]; expect(result[0]).to.deep.equal(expected); }); it('binds named parameters array handles escaped $$', async function() { const typeCast = dialect === 'postgres' || dialect === 'db2' ? '::int' : ''; let logSql; - const result = await this.sequelize.query(`select $1${typeCast} as foo, '$$ / $$1' as bar`, { raw: true, bind: [1], logging(s) { logSql = s;} }); - const expected = dialect === 'db2' ? [{ FOO: 1, BAR: '$ / $1' }] : [{ foo: 1, bar: '$ / $1' }]; + const result = await this.sequelize.query(`select $1${typeCast} as foo, '$$ / $$1' as bar${Support.addDualInSelect()}`, { raw: true, bind: [1], logging(s) { logSql = s;} }); + const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: '$ / $1' }] : [{ foo: 1, bar: '$ / $1' }]; expect(result[0]).to.deep.equal(expected); if (['postgres', 'sqlite', 'db2'].includes(dialect)) { expect(logSql).to.include('$1'); @@ -715,22 +729,23 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { it('binds named parameters object handles escaped $$', async function() { const typeCast = dialect === 'postgres' || dialect === 'db2' ? '::int' : ''; - const result = await this.sequelize.query(`select $one${typeCast} as foo, '$$ / $$one' as bar`, { raw: true, bind: { one: 1 } }); - const expected = dialect === 'db2' ? [{ FOO: 1, BAR: '$ / $one' }] : [{ foo: 1, bar: '$ / $one' }]; + const result = await this.sequelize.query(`select $one${typeCast} as foo, '$$ / $$one' as bar${Support.addDualInSelect()}`, { raw: true, bind: { one: 1 } }); + const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: '$ / $one' }] : [{ foo: 1, bar: '$ / $one' }]; expect(result[0]).to.deep.equal(expected); }); it('escape where has $ on the middle of characters', async function() { const typeCast = dialect === 'postgres' || dialect === 'db2' ? '::int' : ''; - const result = await this.sequelize.query(`select $one${typeCast} as foo$bar`, { raw: true, bind: { one: 1 } }); - const expected = dialect === 'db2' ? [{ FOO$BAR: 1 }] : [{ foo$bar: 1 }]; + const result = await this.sequelize.query(`select $one${typeCast} as foo$bar${Support.addDualInSelect()}`, { raw: true, bind: { one: 1 } }); + const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO$BAR: 1 }] : [{ foo$bar: 1 }]; expect(result[0]).to.deep.equal(expected); }); - if (['postgres', 'sqlite', 'mssql'].includes(dialect)) { + if (['postgres', 'sqlite', 'mssql', 'oracle'].includes(dialect)) { it('does not improperly escape arrays of strings bound to named parameters', async function() { - const result = await this.sequelize.query('select :stringArray as foo', { raw: true, replacements: { stringArray: ['"string"'] } }); - expect(result[0]).to.deep.equal([{ foo: '"string"' }]); + const result = await this.sequelize.query(`select :stringArray as foo${ Support.addDualInSelect()}`, { raw: true, replacements: { stringArray: ['"string"'] } }); + const expectedData = dialect !== 'oracle' ? { foo: '"string"' } : { FOO: '"string"' }; + expect(result[0]).to.deep.equal([expectedData]); }); } @@ -738,9 +753,11 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { let datetime = dialect === 'sqlite' ? 'date(\'now\')' : 'NOW()'; if (dialect === 'mssql') { datetime = 'GETDATE()'; + } else if (dialect === 'oracle') { + datetime = 'SYSDATE'; } - const [result] = await this.sequelize.query(`SELECT ${datetime} AS t`); + const [result] = await this.sequelize.query(`SELECT ${datetime} AS t${Support.addDualInSelect()}`); expect(moment(result[0].t).isValid()).to.be.true; }); diff --git a/test/integration/sequelize/truncate.test.js b/test/integration/sequelize/truncate.test.js index 63f69c2d2d19..8ae4ff79161b 100644 --- a/test/integration/sequelize/truncate.test.js +++ b/test/integration/sequelize/truncate.test.js @@ -7,7 +7,7 @@ const { sequelize } = require('../support'); describe('Sequelize#truncate', () => { // These dialects do not support the CASCADE option on TRUNCATE, so it's impossible to clear // tables that reference each-other. - if (!['mysql', 'mariadb', 'mssql', 'db2'].includes(sequelize.dialect.name)) { + if (!['mysql', 'mariadb', 'mssql', 'db2', 'oracle'].includes(sequelize.dialect.name)) { it('supports truncating cyclic associations with { cascade: true }', async () => { const A = sequelize.define('A', { BId: { type: DataTypes.INTEGER } diff --git a/test/integration/timezone.test.js b/test/integration/timezone.test.js index 9c17d7b205c2..a4a915842487 100644 --- a/test/integration/timezone.test.js +++ b/test/integration/timezone.test.js @@ -30,6 +30,11 @@ if (dialect !== 'sqlite') { if (dialect === 'db2') { query = `SELECT ${now} as "now"`; } + + if (dialect === 'oracle') { + query = 'SELECT sysdate AS "now" FROM DUAL'; + } + const [now1, now2] = await Promise.all([ this.sequelize.query(query, { type: this.sequelize.QueryTypes.SELECT }), this.sequelizeWithTimezone.query(query, { type: this.sequelize.QueryTypes.SELECT }) diff --git a/test/integration/transaction.test.js b/test/integration/transaction.test.js index 9de28f693b8b..06767ce64283 100644 --- a/test/integration/transaction.test.js +++ b/test/integration/transaction.test.js @@ -88,7 +88,7 @@ if (current.dialect.supports.transactions) { await this.sequelize.transaction(t => { transaction = t; transaction.afterCommit(hook); - return this.sequelize.query('SELECT 1+1', { transaction, type: QueryTypes.SELECT }); + return this.sequelize.query(`SELECT 1+1${ Support.addDualInSelect()}`, { transaction, type: QueryTypes.SELECT }); }); expect(hook).to.have.been.calledOnce; @@ -189,24 +189,24 @@ if (current.dialect.supports.transactions) { it('does not allow queries after commit', async function() { const t = await this.sequelize.transaction(); - await this.sequelize.query('SELECT 1+1', { transaction: t, raw: true }); + await this.sequelize.query(`SELECT 1+1${ Support.addDualInSelect()}`, { transaction: t, raw: true }); await t.commit(); - await expect(this.sequelize.query('SELECT 1+1', { transaction: t, raw: true })).to.be.eventually.rejectedWith( + await expect(this.sequelize.query(`SELECT 1+1${ Support.addDualInSelect()}`, { transaction: t, raw: true })).to.be.eventually.rejectedWith( Error, /commit has been called on this transaction\([^)]+\), you can no longer use it\. \(The rejected query is attached as the 'sql' property of this error\)/ - ).and.have.deep.property('sql').that.equal('SELECT 1+1'); + ).and.have.deep.property('sql').that.equal(`SELECT 1+1${ Support.addDualInSelect()}`); }); it('does not allow queries immediately after commit call', async function() { await expect((async () => { const t = await this.sequelize.transaction(); - await this.sequelize.query('SELECT 1+1', { transaction: t, raw: true }); + await this.sequelize.query(`SELECT 1+1${ Support.addDualInSelect()}`, { transaction: t, raw: true }); await Promise.all([ expect(t.commit()).to.eventually.be.fulfilled, - expect(this.sequelize.query('SELECT 1+1', { transaction: t, raw: true })).to.be.eventually.rejectedWith( + expect(this.sequelize.query(`SELECT 1+1${ Support.addDualInSelect()}`, { transaction: t, raw: true })).to.be.eventually.rejectedWith( Error, /commit has been called on this transaction\([^)]+\), you can no longer use it\. \(The rejected query is attached as the 'sql' property of this error\)/ - ).and.have.deep.property('sql').that.equal('SELECT 1+1') + ).and.have.deep.property('sql').that.equal(`SELECT 1+1${ Support.addDualInSelect()}`) ]); })()).to.be.eventually.fulfilled; }); @@ -780,7 +780,7 @@ if (current.dialect.supports.transactions) { } // PostgreSQL is excluded because it detects Serialization Failure on commit instead of acquiring locks on the read rows - if (!['sqlite', 'postgres', 'postgres-native', 'db2'].includes(dialect)) { + if (!['sqlite', 'postgres', 'postgres-native', 'db2', 'oracle'].includes(dialect)) { it('should block updates after reading a row using SERIALIZABLE', async function() { const User = this.sequelize.define('user', { username: Support.Sequelize.STRING diff --git a/test/integration/utils.test.js b/test/integration/utils.test.js index 0b65c75f4dbe..d3f502d89234 100644 --- a/test/integration/utils.test.js +++ b/test/integration/utils.test.js @@ -160,7 +160,7 @@ describe(Support.getTestDialectTeaser('Utils'), () => { ]); }); - if (Support.getTestDialect() !== 'mssql') { + if (Support.getTestDialect() !== 'mssql' && Support.getTestDialect() !== 'oracle') { it('accepts condition object (with cast)', async function() { const type = Support.getTestDialect() === 'mysql' ? 'unsigned' : 'int'; @@ -188,7 +188,7 @@ describe(Support.getTestDialectTeaser('Utils'), () => { }); } - if (Support.getTestDialect() !== 'mssql' && Support.getTestDialect() !== 'postgres') { + if (Support.getTestDialect() !== 'mssql' && Support.getTestDialect() !== 'postgres' && Support.getTestDialect() !== 'oracle') { it('accepts condition object (auto casting)', async function() { const [airplane] = await Airplane.findAll({ attributes: [ diff --git a/test/support.js b/test/support.js index fe621ec9a909..96ab14c4f870 100644 --- a/test/support.js +++ b/test/support.js @@ -275,6 +275,10 @@ const Support = { .replace(/\( /g, '(') // remove whitespace at start & end .trim(); + }, + + addDualInSelect() { + return this.getTestDialect() === 'oracle' ? ' FROM DUAL' : ''; } }; diff --git a/test/unit/dialect-module-configuration.test.js b/test/unit/dialect-module-configuration.test.js index 8758d26cd79b..5d6017a6f031 100644 --- a/test/unit/dialect-module-configuration.test.js +++ b/test/unit/dialect-module-configuration.test.js @@ -33,6 +33,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { case 'mssql': dialectPath = path.join(dialectPath, 'tedious'); break; case 'sqlite': dialectPath = path.join(dialectPath, 'sqlite3'); break; case 'snowflake': dialectPath = path.join(dialectPath, 'snowflake-sdk'); break; + case 'oracle': dialectPath = path.join(dialectPath, 'oracledb'); break; default: throw Error('Unsupported dialect'); } diff --git a/test/unit/dialects/abstract/query-generator.test.js b/test/unit/dialects/abstract/query-generator.test.js index 3ed56dd17ba1..d60163888e56 100644 --- a/test/unit/dialects/abstract/query-generator.test.js +++ b/test/unit/dialects/abstract/query-generator.test.js @@ -153,6 +153,7 @@ describe('QueryGenerator', () => { mysql: query => expect(query()).to.equal("json_unquote(json_extract(profile,'$.\\\"id\\\"'))"), mssql: query => expect(query).to.throw(Error), snowflake: query => expect(query).to.throw(Error), + oracle: query => expect(query).to.throw(Error), db2: query => expect(query).to.throw(Error) }); }); @@ -166,6 +167,7 @@ describe('QueryGenerator', () => { mysql: query => expect(query()).to.equal("json_unquote(json_extract(profile,'$.\\\"id\\\"'))"), mssql: query => expect(query).to.throw(Error), snowflake: query => expect(query).to.throw(Error), + oracle: query => expect(query).to.throw(Error), db2: query => expect(query).to.throw(Error) }); }); @@ -179,6 +181,7 @@ describe('QueryGenerator', () => { mysql: query => expect(query()).to.equal("json_unquote(json_extract(profile,'$.\\\"id\\\"'))"), mssql: query => expect(query).to.throw(Error), snowflake: query => expect(query).to.throw(Error), + oracle: query => expect(query).to.throw(Error), db2: query => expect(query).to.throw(Error) }); }); diff --git a/test/unit/esm-named-exports.test.js b/test/unit/esm-named-exports.test.js index 7afcd627740d..5c3d65ae4a1f 100644 --- a/test/unit/esm-named-exports.test.js +++ b/test/unit/esm-named-exports.test.js @@ -46,6 +46,7 @@ describe('ESM module', () => { 'mariadb', 'sqlite', 'snowflake', + 'oracle', 'db2', 'mssql', '_setupHooks', diff --git a/test/unit/query-interface/bulk-insert.test.js b/test/unit/query-interface/bulk-insert.test.js index 6ab4aa7dc405..1243ea063de6 100644 --- a/test/unit/query-interface/bulk-insert.test.js +++ b/test/unit/query-interface/bulk-insert.test.js @@ -1,6 +1,7 @@ const { DataTypes } = require('sequelize'); const sinon = require('sinon'); const { expectsql, sequelize } = require('../../support'); +const dialect = require('../support').getTestDialect(); const { stubQueryRun } = require('./stub-query-run'); describe('QueryInterface#bulkInsert', () => { @@ -13,7 +14,7 @@ describe('QueryInterface#bulkInsert', () => { }); // you'll find more replacement tests in query-generator tests - it('does not parse replacements outside of raw sql', async () => { + (dialect !== 'oracle' ? it : it.skip)('does not parse replacements outside of raw sql', async () => { const getSql = stubQueryRun(); await sequelize.getQueryInterface().bulkInsert(User.tableName, [{ diff --git a/test/unit/query-interface/raw-select.test.js b/test/unit/query-interface/raw-select.test.js index 8404ef27bf85..3ec37ec1cb4e 100644 --- a/test/unit/query-interface/raw-select.test.js +++ b/test/unit/query-interface/raw-select.test.js @@ -29,6 +29,7 @@ describe('QueryInterface#rawSelect', () => { expectsql(getSql(), { default: 'SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = \'some :data\';', + oracle: 'SELECT "id" FROM "Users" "User" WHERE "User"."username" = \'some :data\';', mssql: 'SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = N\'some :data\';' }); }); diff --git a/test/unit/query-interface/select.test.js b/test/unit/query-interface/select.test.js index 4e04109a3cf9..d3968c9bc210 100644 --- a/test/unit/query-interface/select.test.js +++ b/test/unit/query-interface/select.test.js @@ -29,6 +29,7 @@ describe('QueryInterface#select', () => { expectsql(getSql(), { default: 'SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = \'some :data\';', + oracle: 'SELECT "id" FROM "Users" "User" WHERE "User"."username" = \'some :data\';', mssql: 'SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = N\'some :data\';' }); }); diff --git a/test/unit/sql/add-constraint.test.js b/test/unit/sql/add-constraint.test.js index 3997b4bee74b..6a3a015e7e39 100644 --- a/test/unit/sql/add-constraint.test.js +++ b/test/unit/sql/add-constraint.test.js @@ -156,7 +156,8 @@ if (current.dialect.supports.constraints.addConstraint) { }); }); - it('supports composite keys', () => { + // The Oracle dialect doesn't support onUpdate cascade + (current.dialect.name !== 'oracle' ? it : it.skip)('supports composite keys', () => { expectsql( sql.addConstraintQuery('myTable', { type: 'foreign key', @@ -175,8 +176,8 @@ if (current.dialect.supports.constraints.addConstraint) { } ); }); - - it('uses onDelete, onUpdate', () => { + // The Oracle dialect doesn't support onUpdate cascade + (current.dialect.name !== 'oracle' ? it : it.skip)('uses onDelete, onUpdate', () => { expectsql(sql.addConstraintQuery('myTable', { type: 'foreign key', fields: ['myColumn'], diff --git a/test/unit/sql/change-column.test.js b/test/unit/sql/change-column.test.js index 45b734e5c0b8..16155f8f6603 100644 --- a/test/unit/sql/change-column.test.js +++ b/test/unit/sql/change-column.test.js @@ -44,6 +44,7 @@ if (current.dialect.name !== 'sqlite') { mariadb: 'ALTER TABLE `users` CHANGE `level_id` `level_id` FLOAT NOT NULL;', mysql: 'ALTER TABLE `users` CHANGE `level_id` `level_id` FLOAT NOT NULL;', postgres: 'ALTER TABLE "users" ALTER COLUMN "level_id" SET NOT NULL;ALTER TABLE "users" ALTER COLUMN "level_id" DROP DEFAULT;ALTER TABLE "users" ALTER COLUMN "level_id" TYPE FLOAT;', + oracle: 'DECLARE CONS_NAME VARCHAR2(200); BEGIN BEGIN EXECUTE IMMEDIATE \'ALTER TABLE "users" MODIFY "level_id" BINARY_FLOAT NOT NULL\'; EXCEPTION WHEN OTHERS THEN IF SQLCODE = -1442 OR SQLCODE = -1451 THEN EXECUTE IMMEDIATE \'ALTER TABLE "users" MODIFY "level_id" BINARY_FLOAT \'; ELSE RAISE; END IF; END; END;', snowflake: 'ALTER TABLE "users" ALTER COLUMN "level_id" SET NOT NULL;ALTER TABLE "users" ALTER COLUMN "level_id" DROP DEFAULT;ALTER TABLE "users" ALTER COLUMN "level_id" TYPE FLOAT;' }); }); @@ -60,6 +61,7 @@ if (current.dialect.name !== 'sqlite') { onDelete: 'cascade' }).then(sql => { expectsql(sql, { + oracle: 'DECLARE CONS_NAME VARCHAR2(200); BEGIN BEGIN select constraint_name into cons_name from ( select distinct cc.owner, cc.table_name, cc.constraint_name, cc.column_name as cons_columns from all_cons_columns cc, all_constraints c where cc.owner = c.owner and cc.table_name = c.table_name and cc.constraint_name = c.constraint_name and c.constraint_type = \'R\' group by cc.owner, cc.table_name, cc.constraint_name, cc.column_name ) where owner = USER and table_name = \'users\' and cons_columns = \'level_id\' ; EXCEPTION WHEN NO_DATA_FOUND THEN CONS_NAME := NULL; END; IF CONS_NAME IS NOT NULL THEN EXECUTE IMMEDIATE \'ALTER TABLE "users" DROP CONSTRAINT "\'||CONS_NAME||\'"\'; END IF; EXECUTE IMMEDIATE \'ALTER TABLE "users" ADD FOREIGN KEY ("level_id") REFERENCES "level" ("id") ON DELETE CASCADE\'; END;', mssql: 'ALTER TABLE [users] ADD FOREIGN KEY ([level_id]) REFERENCES [level] ([id]) ON DELETE CASCADE;', db2: 'ALTER TABLE "users" ADD CONSTRAINT "level_id_foreign_idx" FOREIGN KEY ("level_id") REFERENCES "level" ("id") ON DELETE CASCADE;', mariadb: 'ALTER TABLE `users` ADD FOREIGN KEY (`level_id`) REFERENCES `level` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;', diff --git a/test/unit/sql/create-table.test.js b/test/unit/sql/create-table.test.js index 6fccb8b11e74..115259520239 100644 --- a/test/unit/sql/create-table.test.js +++ b/test/unit/sql/create-table.test.js @@ -27,6 +27,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'CREATE TABLE IF NOT EXISTS "foo"."users" ("id" SERIAL , "mood" "foo"."enum_users_mood", PRIMARY KEY ("id"));', mariadb: "CREATE TABLE IF NOT EXISTS `foo`.`users` (`id` INTEGER NOT NULL auto_increment , `mood` ENUM('happy', 'sad'), PRIMARY KEY (`id`)) ENGINE=InnoDB;", mysql: "CREATE TABLE IF NOT EXISTS `foo.users` (`id` INTEGER NOT NULL auto_increment , `mood` ENUM('happy', 'sad'), PRIMARY KEY (`id`)) ENGINE=InnoDB;", + oracle: 'BEGIN EXECUTE IMMEDIATE \'CREATE TABLE "foo"."users" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY, "mood" VARCHAR2(512),CONSTRAINT "PK_foo.users_id" PRIMARY KEY ("id"), CHECK ("mood" IN(\'\'happy\'\', \'\'sad\'\')))\'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;', mssql: "IF OBJECT_ID('[foo].[users]', 'U') IS NULL CREATE TABLE [foo].[users] ([id] INTEGER NOT NULL IDENTITY(1,1) , [mood] VARCHAR(255) CHECK ([mood] IN(N'happy', N'sad')), PRIMARY KEY ([id]));" }); }); @@ -52,6 +53,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { it('references right schema when adding foreign key #9029', () => { expectsql(sql.createTableQuery(BarProject.getTableName(), sql.attributesToSQL(BarProject.rawAttributes), { }), { + oracle: 'BEGIN EXECUTE IMMEDIATE \'CREATE TABLE "bar"."projects" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY, "user_id" INTEGER NULL,CONSTRAINT "PK_bar.projects_id" PRIMARY KEY ("id"),FOREIGN KEY ("user_id") REFERENCES "bar"."users" ("id") )\'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;', sqlite: 'CREATE TABLE IF NOT EXISTS `bar.projects` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `user_id` INTEGER REFERENCES `bar.users` (`id`) ON DELETE NO ACTION ON UPDATE CASCADE);', db2: 'CREATE TABLE "bar"."projects" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , "user_id" INTEGER, PRIMARY KEY ("id"), FOREIGN KEY ("user_id") REFERENCES "bar"."users" ("id") ON DELETE NO ACTION);', postgres: 'CREATE TABLE IF NOT EXISTS "bar"."projects" ("id" SERIAL , "user_id" INTEGER REFERENCES "bar"."users" ("id") ON DELETE NO ACTION ON UPDATE CASCADE, PRIMARY KEY ("id"));', @@ -85,6 +87,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'CREATE TABLE "images" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , PRIMARY KEY ("id"), FOREIGN KEY ("id") REFERENCES "files" ("id"));', mariadb: 'CREATE TABLE IF NOT EXISTS `images` (`id` INTEGER auto_increment , PRIMARY KEY (`id`), FOREIGN KEY (`id`) REFERENCES `files` (`id`)) ENGINE=InnoDB;', mysql: 'CREATE TABLE IF NOT EXISTS `images` (`id` INTEGER auto_increment , PRIMARY KEY (`id`), FOREIGN KEY (`id`) REFERENCES `files` (`id`)) ENGINE=InnoDB;', + oracle: 'BEGIN EXECUTE IMMEDIATE \'CREATE TABLE "images" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY ,CONSTRAINT "PK_images_id" PRIMARY KEY ("id"),FOREIGN KEY ("id") REFERENCES "files" ("id"))\'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;', mssql: 'IF OBJECT_ID(\'[images]\', \'U\') IS NULL CREATE TABLE [images] ([id] INTEGER IDENTITY(1,1) , PRIMARY KEY ([id]), FOREIGN KEY ([id]) REFERENCES [files] ([id]));' }); }); diff --git a/test/unit/sql/data-types.test.js b/test/unit/sql/data-types.test.js index 2ab7e08f6eb1..35f36d9dd99b 100644 --- a/test/unit/sql/data-types.test.js +++ b/test/unit/sql/data-types.test.js @@ -25,17 +25,20 @@ describe(Support.getTestDialectTeaser('SQL'), () => { describe('STRING', () => { testsql('STRING', DataTypes.STRING, { default: 'VARCHAR(255)', - mssql: 'NVARCHAR(255)' + mssql: 'NVARCHAR(255)', + oracle: 'NVARCHAR2(255)' }); testsql('STRING(1234)', DataTypes.STRING(1234), { default: 'VARCHAR(1234)', - mssql: 'NVARCHAR(1234)' + mssql: 'NVARCHAR(1234)', + oracle: 'NVARCHAR2(1234)' }); testsql('STRING({ length: 1234 })', DataTypes.STRING({ length: 1234 }), { default: 'VARCHAR(1234)', - mssql: 'NVARCHAR(1234)' + mssql: 'NVARCHAR(1234)', + oracle: 'NVARCHAR2(1234)' }); testsql('STRING(1234).BINARY', DataTypes.STRING(1234).BINARY, { @@ -43,7 +46,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'VARCHAR(1234) FOR BIT DATA', sqlite: 'VARCHAR BINARY(1234)', mssql: 'BINARY(1234)', - postgres: 'BYTEA' + postgres: 'BYTEA', + oracle: 'RAW(1234)' }); testsql('STRING.BINARY', DataTypes.STRING.BINARY, { @@ -51,7 +55,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'VARCHAR(255) FOR BIT DATA', sqlite: 'VARCHAR BINARY(255)', mssql: 'BINARY(255)', - postgres: 'BYTEA' + postgres: 'BYTEA', + oracle: 'RAW(255)' }); describe('validate', () => { @@ -69,6 +74,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('TEXT', DataTypes.TEXT, { default: 'TEXT', db2: 'VARCHAR(32672)', + oracle: 'CLOB', mssql: 'NVARCHAR(MAX)' // in mssql text is actually representing a non unicode text field }); @@ -77,6 +83,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mssql: 'NVARCHAR(256)', db2: 'VARCHAR(256)', mariadb: 'TINYTEXT', + oracle: 'CLOB', mysql: 'TINYTEXT' }); @@ -85,6 +92,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mssql: 'NVARCHAR(256)', db2: 'VARCHAR(256)', mariadb: 'TINYTEXT', + oracle: 'CLOB', mysql: 'TINYTEXT' }); @@ -93,6 +101,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mssql: 'NVARCHAR(MAX)', db2: 'VARCHAR(8192)', mariadb: 'MEDIUMTEXT', + oracle: 'CLOB', mysql: 'MEDIUMTEXT' }); @@ -101,6 +110,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mssql: 'NVARCHAR(MAX)', db2: 'CLOB(65536)', mariadb: 'LONGTEXT', + oracle: 'CLOB', mysql: 'LONGTEXT' }); @@ -136,12 +146,14 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('CHAR(12).BINARY', DataTypes.CHAR(12).BINARY, { default: 'CHAR(12) BINARY', + oracle: 'RAW(12)', sqlite: 'CHAR BINARY(12)', postgres: 'BYTEA' }); testsql('CHAR.BINARY', DataTypes.CHAR.BINARY, { default: 'CHAR(255) BINARY', + oracle: 'RAW(255)', sqlite: 'CHAR BINARY(255)', postgres: 'BYTEA' }); @@ -155,6 +167,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: 'TINYINT(1)', mysql: 'TINYINT(1)', sqlite: 'TINYINT(1)', + oracle: 'CHAR(1)', snowflake: 'BOOLEAN' }); @@ -188,6 +201,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: 'DATETIME', db2: 'TIMESTAMP', sqlite: 'DATETIME', + oracle: 'TIMESTAMP WITH LOCAL TIME ZONE', snowflake: 'TIMESTAMP' }); @@ -198,6 +212,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: 'DATETIME(6)', db2: 'TIMESTAMP(6)', sqlite: 'DATETIME', + oracle: 'TIMESTAMP WITH LOCAL TIME ZONE', snowflake: 'TIMESTAMP' }); @@ -246,6 +261,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: 'CHAR(36) BINARY', mysql: 'CHAR(36) BINARY', sqlite: 'UUID', + oracle: 'VARCHAR2(36)', snowflake: 'VARCHAR(36)' }); @@ -345,6 +361,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('NOW', DataTypes.NOW, { default: 'NOW', db2: 'CURRENT TIME', + oracle: 'SYSDATE', mssql: 'GETDATE()' }); }); @@ -448,6 +465,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { title: 'TINYINT', dataType: DataTypes.TINYINT, expect: { + oracle: 'NUMBER(3)', default: 'TINYINT' } }, @@ -457,6 +475,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { expect: { default: 'TINYINT(2)', db2: 'TINYINT', + oracle: 'NUMBER(3)', mssql: 'TINYINT', postgres: 'TINYINT' } @@ -467,6 +486,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { expect: { default: 'TINYINT(2)', db2: 'TINYINT', + oracle: 'NUMBER(3)', mssql: 'TINYINT', postgres: 'TINYINT' } @@ -477,6 +497,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { expect: { default: 'TINYINT UNSIGNED', db2: 'TINYINT', + oracle: 'NUMBER(3)', mssql: 'TINYINT', postgres: 'TINYINT', sqlite: 'TINYINT' @@ -488,6 +509,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { expect: { default: 'TINYINT(2) UNSIGNED', db2: 'TINYINT', + oracle: 'NUMBER(3)', sqlite: 'TINYINT(2)', mssql: 'TINYINT', postgres: 'TINYINT' @@ -499,6 +521,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { expect: { default: 'TINYINT UNSIGNED ZEROFILL', db2: 'TINYINT', + oracle: 'NUMBER(3)', mssql: 'TINYINT', postgres: 'TINYINT', sqlite: 'TINYINT' @@ -510,6 +533,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { expect: { default: 'TINYINT(2) UNSIGNED ZEROFILL', db2: 'TINYINT', + oracle: 'NUMBER(3)', sqlite: 'TINYINT(2)', mssql: 'TINYINT', postgres: 'TINYINT' @@ -521,6 +545,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { expect: { default: 'TINYINT ZEROFILL', db2: 'TINYINT', + oracle: 'NUMBER(3)', mssql: 'TINYINT', postgres: 'TINYINT', sqlite: 'TINYINT' @@ -532,6 +557,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { expect: { default: 'TINYINT(2) ZEROFILL', db2: 'TINYINT', + oracle: 'NUMBER(3)', sqlite: 'TINYINT(2)', mssql: 'TINYINT', postgres: 'TINYINT' @@ -543,6 +569,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { expect: { default: 'TINYINT UNSIGNED ZEROFILL', db2: 'TINYINT', + oracle: 'NUMBER(3)', mssql: 'TINYINT', postgres: 'TINYINT', sqlite: 'TINYINT' @@ -554,6 +581,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { expect: { default: 'TINYINT(2) UNSIGNED ZEROFILL', db2: 'TINYINT', + oracle: 'NUMBER(3)', sqlite: 'TINYINT(2)', mssql: 'TINYINT', postgres: 'TINYINT' @@ -592,6 +620,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { title: 'SMALLINT', dataType: DataTypes.SMALLINT, expect: { + oracle: 'NUMBER(5)', default: 'SMALLINT' } }, @@ -600,6 +629,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.SMALLINT(4), expect: { default: 'SMALLINT(4)', + oracle: 'NUMBER(5)', postgres: 'SMALLINT', db2: 'SMALLINT', mssql: 'SMALLINT' @@ -610,6 +640,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.SMALLINT({ length: 4 }), expect: { default: 'SMALLINT(4)', + oracle: 'NUMBER(5)', postgres: 'SMALLINT', db2: 'SMALLINT', mssql: 'SMALLINT' @@ -620,6 +651,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.SMALLINT.UNSIGNED, expect: { default: 'SMALLINT UNSIGNED', + oracle: 'NUMBER(5)', postgres: 'SMALLINT', db2: 'SMALLINT', mssql: 'SMALLINT', @@ -631,6 +663,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.SMALLINT(4).UNSIGNED, expect: { default: 'SMALLINT(4) UNSIGNED', + oracle: 'NUMBER(5)', sqlite: 'SMALLINT(4)', postgres: 'SMALLINT', db2: 'SMALLINT', @@ -642,6 +675,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.SMALLINT.UNSIGNED.ZEROFILL, expect: { default: 'SMALLINT UNSIGNED ZEROFILL', + oracle: 'NUMBER(5)', postgres: 'SMALLINT', db2: 'SMALLINT', mssql: 'SMALLINT', @@ -653,6 +687,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.SMALLINT(4).UNSIGNED.ZEROFILL, expect: { default: 'SMALLINT(4) UNSIGNED ZEROFILL', + oracle: 'NUMBER(5)', sqlite: 'SMALLINT(4)', postgres: 'SMALLINT', db2: 'SMALLINT', @@ -664,6 +699,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.SMALLINT.ZEROFILL, expect: { default: 'SMALLINT ZEROFILL', + oracle: 'NUMBER(5)', postgres: 'SMALLINT', db2: 'SMALLINT', mssql: 'SMALLINT', @@ -675,6 +711,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.SMALLINT(4).ZEROFILL, expect: { default: 'SMALLINT(4) ZEROFILL', + oracle: 'NUMBER(5)', sqlite: 'SMALLINT(4)', postgres: 'SMALLINT', db2: 'SMALLINT', @@ -686,6 +723,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.SMALLINT.ZEROFILL.UNSIGNED, expect: { default: 'SMALLINT UNSIGNED ZEROFILL', + oracle: 'NUMBER(5)', postgres: 'SMALLINT', db2: 'SMALLINT', mssql: 'SMALLINT', @@ -697,6 +735,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.SMALLINT(4).ZEROFILL.UNSIGNED, expect: { default: 'SMALLINT(4) UNSIGNED ZEROFILL', + oracle: 'NUMBER(5)', sqlite: 'SMALLINT(4)', postgres: 'SMALLINT', db2: 'SMALLINT', @@ -736,6 +775,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { title: 'MEDIUMINT', dataType: DataTypes.MEDIUMINT, expect: { + oracle: 'NUMBER(8)', default: 'MEDIUMINT' } }, @@ -743,6 +783,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { title: 'MEDIUMINT(6)', dataType: DataTypes.MEDIUMINT(6), expect: { + oracle: 'NUMBER(8)', default: 'MEDIUMINT(6)' } }, @@ -750,6 +791,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { title: 'MEDIUMINT({ length: 6 })', dataType: DataTypes.MEDIUMINT({ length: 6 }), expect: { + oracle: 'NUMBER(8)', default: 'MEDIUMINT(6)' } }, @@ -758,6 +800,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.MEDIUMINT.UNSIGNED, expect: { default: 'MEDIUMINT UNSIGNED', + oracle: 'NUMBER(8)', sqlite: 'MEDIUMINT' } }, @@ -766,6 +809,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.MEDIUMINT(6).UNSIGNED, expect: { default: 'MEDIUMINT(6) UNSIGNED', + oracle: 'NUMBER(8)', sqlite: 'MEDIUMINT(6)' } }, @@ -774,6 +818,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.MEDIUMINT.UNSIGNED.ZEROFILL, expect: { default: 'MEDIUMINT UNSIGNED ZEROFILL', + oracle: 'NUMBER(8)', sqlite: 'MEDIUMINT' } }, @@ -782,6 +827,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.MEDIUMINT(6).UNSIGNED.ZEROFILL, expect: { default: 'MEDIUMINT(6) UNSIGNED ZEROFILL', + oracle: 'NUMBER(8)', sqlite: 'MEDIUMINT(6)' } }, @@ -790,6 +836,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.MEDIUMINT.ZEROFILL, expect: { default: 'MEDIUMINT ZEROFILL', + oracle: 'NUMBER(8)', sqlite: 'MEDIUMINT' } }, @@ -798,6 +845,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.MEDIUMINT(6).ZEROFILL, expect: { default: 'MEDIUMINT(6) ZEROFILL', + oracle: 'NUMBER(8)', sqlite: 'MEDIUMINT(6)' } }, @@ -806,6 +854,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.MEDIUMINT.ZEROFILL.UNSIGNED, expect: { default: 'MEDIUMINT UNSIGNED ZEROFILL', + oracle: 'NUMBER(8)', sqlite: 'MEDIUMINT' } }, @@ -814,6 +863,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { dataType: DataTypes.MEDIUMINT(6).ZEROFILL.UNSIGNED, expect: { default: 'MEDIUMINT(6) UNSIGNED ZEROFILL', + oracle: 'NUMBER(8)', sqlite: 'MEDIUMINT(6)' } } @@ -846,7 +896,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { describe('BIGINT', () => { testsql('BIGINT', DataTypes.BIGINT, { - default: 'BIGINT' + default: 'BIGINT', + oracle: 'NUMBER(19)' }); testsql('BIGINT.UNSIGNED', DataTypes.BIGINT.UNSIGNED, { @@ -854,7 +905,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'BIGINT', db2: 'BIGINT', mssql: 'BIGINT', - sqlite: 'BIGINT' + sqlite: 'BIGINT', + oracle: 'NUMBER(19)' }); testsql('BIGINT.UNSIGNED.ZEROFILL', DataTypes.BIGINT.UNSIGNED.ZEROFILL, { @@ -862,13 +914,15 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'BIGINT', db2: 'BIGINT', mssql: 'BIGINT', - sqlite: 'BIGINT' + sqlite: 'BIGINT', + oracle: 'NUMBER(19)' }); testsql('BIGINT(11)', DataTypes.BIGINT(11), { default: 'BIGINT(11)', postgres: 'BIGINT', db2: 'BIGINT', + oracle: 'NUMBER(19)', mssql: 'BIGINT' }); @@ -876,6 +930,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'BIGINT(11)', postgres: 'BIGINT', db2: 'BIGINT', + oracle: 'NUMBER(19)', mssql: 'BIGINT' }); @@ -884,6 +939,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'BIGINT(11)', postgres: 'BIGINT', db2: 'BIGINT', + oracle: 'NUMBER(19)', mssql: 'BIGINT' }); @@ -892,6 +948,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'BIGINT(11)', postgres: 'BIGINT', db2: 'BIGINT', + oracle: 'NUMBER(19)', mssql: 'BIGINT' }); @@ -900,6 +957,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'BIGINT(11)', postgres: 'BIGINT', db2: 'BIGINT', + oracle: 'NUMBER(19)', mssql: 'BIGINT' }); @@ -908,6 +966,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'BIGINT(11)', postgres: 'BIGINT', db2: 'BIGINT', + oracle: 'NUMBER(19)', mssql: 'BIGINT' }); @@ -934,6 +993,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { describe('REAL', () => { testsql('REAL', DataTypes.REAL, { + oracle: 'BINARY_DOUBLE', default: 'REAL' }); @@ -941,6 +1001,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'REAL UNSIGNED', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); @@ -948,6 +1009,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'REAL(11)', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); @@ -955,6 +1017,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'REAL(11)', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); @@ -963,6 +1026,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'REAL UNSIGNED(11)', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); @@ -971,6 +1035,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'REAL UNSIGNED ZEROFILL(11)', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); @@ -979,6 +1044,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'REAL ZEROFILL(11)', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); @@ -987,6 +1053,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'REAL UNSIGNED ZEROFILL(11)', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); @@ -994,6 +1061,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'REAL(11,12)', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); @@ -1002,6 +1070,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'REAL UNSIGNED(11,12)', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); @@ -1010,6 +1079,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'REAL UNSIGNED(11,12)', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); @@ -1018,6 +1088,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'REAL UNSIGNED ZEROFILL(11,12)', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); @@ -1026,6 +1097,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'REAL ZEROFILL(11,12)', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); @@ -1034,6 +1106,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'REAL UNSIGNED ZEROFILL(11,12)', postgres: 'REAL', db2: 'REAL', + oracle: 'BINARY_DOUBLE', mssql: 'REAL' }); }); @@ -1041,18 +1114,21 @@ describe(Support.getTestDialectTeaser('SQL'), () => { describe('DOUBLE PRECISION', () => { testsql('DOUBLE', DataTypes.DOUBLE, { db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', default: 'DOUBLE PRECISION' }); testsql('DOUBLE.UNSIGNED', DataTypes.DOUBLE.UNSIGNED, { default: 'DOUBLE PRECISION UNSIGNED', db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', postgres: 'DOUBLE PRECISION' }); testsql('DOUBLE(11)', DataTypes.DOUBLE(11), { default: 'DOUBLE PRECISION(11)', db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', postgres: 'DOUBLE PRECISION' }); @@ -1060,6 +1136,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'DOUBLE PRECISION(11) UNSIGNED', sqlite: 'DOUBLE PRECISION UNSIGNED(11)', db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', postgres: 'DOUBLE PRECISION' }); @@ -1067,6 +1144,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'DOUBLE PRECISION(11) UNSIGNED', sqlite: 'DOUBLE PRECISION UNSIGNED(11)', db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', postgres: 'DOUBLE PRECISION' }); @@ -1074,6 +1152,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'DOUBLE PRECISION(11) UNSIGNED ZEROFILL', sqlite: 'DOUBLE PRECISION UNSIGNED ZEROFILL(11)', db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', postgres: 'DOUBLE PRECISION' }); @@ -1081,6 +1160,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'DOUBLE PRECISION(11) ZEROFILL', sqlite: 'DOUBLE PRECISION ZEROFILL(11)', db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', postgres: 'DOUBLE PRECISION' }); @@ -1088,12 +1168,14 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'DOUBLE PRECISION(11) UNSIGNED ZEROFILL', sqlite: 'DOUBLE PRECISION UNSIGNED ZEROFILL(11)', db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', postgres: 'DOUBLE PRECISION' }); testsql('DOUBLE(11, 12)', DataTypes.DOUBLE(11, 12), { default: 'DOUBLE PRECISION(11,12)', db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', postgres: 'DOUBLE PRECISION' }); @@ -1101,6 +1183,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'DOUBLE PRECISION(11,12) UNSIGNED', sqlite: 'DOUBLE PRECISION UNSIGNED(11,12)', db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', postgres: 'DOUBLE PRECISION' }); @@ -1108,6 +1191,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'DOUBLE PRECISION(11,12) UNSIGNED ZEROFILL', sqlite: 'DOUBLE PRECISION UNSIGNED ZEROFILL(11,12)', db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', postgres: 'DOUBLE PRECISION' }); @@ -1115,6 +1199,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'DOUBLE PRECISION(11,12) ZEROFILL', sqlite: 'DOUBLE PRECISION ZEROFILL(11,12)', db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', postgres: 'DOUBLE PRECISION' }); @@ -1122,6 +1207,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: 'DOUBLE PRECISION(11,12) UNSIGNED ZEROFILL', sqlite: 'DOUBLE PRECISION UNSIGNED ZEROFILL(11,12)', db2: 'DOUBLE', + oracle: 'BINARY_DOUBLE', postgres: 'DOUBLE PRECISION' }); }); @@ -1129,11 +1215,13 @@ describe(Support.getTestDialectTeaser('SQL'), () => { describe('FLOAT', () => { testsql('FLOAT', DataTypes.FLOAT, { default: 'FLOAT', + oracle: 'BINARY_FLOAT', postgres: 'FLOAT' }); testsql('FLOAT.UNSIGNED', DataTypes.FLOAT.UNSIGNED, { default: 'FLOAT UNSIGNED', + oracle: 'BINARY_FLOAT', postgres: 'FLOAT', db2: 'FLOAT', mssql: 'FLOAT' @@ -1141,6 +1229,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('FLOAT(11)', DataTypes.FLOAT(11), { default: 'FLOAT(11)', + oracle: 'BINARY_FLOAT', postgres: 'FLOAT(11)', // 1-24 = 4 bytes; 35-53 = 8 bytes db2: 'FLOAT(11)', // 1-24 = 4 bytes; 35-53 = 8 bytes mssql: 'FLOAT(11)' // 1-24 = 4 bytes; 35-53 = 8 bytes @@ -1148,6 +1237,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('FLOAT(11).UNSIGNED', DataTypes.FLOAT(11).UNSIGNED, { default: 'FLOAT(11) UNSIGNED', + oracle: 'BINARY_FLOAT', sqlite: 'FLOAT UNSIGNED(11)', postgres: 'FLOAT(11)', db2: 'FLOAT(11)', @@ -1156,6 +1246,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('FLOAT(11).UNSIGNED.ZEROFILL', DataTypes.FLOAT(11).UNSIGNED.ZEROFILL, { default: 'FLOAT(11) UNSIGNED ZEROFILL', + oracle: 'BINARY_FLOAT', sqlite: 'FLOAT UNSIGNED ZEROFILL(11)', postgres: 'FLOAT(11)', db2: 'FLOAT(11)', @@ -1164,6 +1255,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('FLOAT(11).ZEROFILL', DataTypes.FLOAT(11).ZEROFILL, { default: 'FLOAT(11) ZEROFILL', + oracle: 'BINARY_FLOAT', sqlite: 'FLOAT ZEROFILL(11)', postgres: 'FLOAT(11)', db2: 'FLOAT(11)', @@ -1172,6 +1264,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('FLOAT({ length: 11 }).ZEROFILL', DataTypes.FLOAT({ length: 11 }).ZEROFILL, { default: 'FLOAT(11) ZEROFILL', + oracle: 'BINARY_FLOAT', sqlite: 'FLOAT ZEROFILL(11)', postgres: 'FLOAT(11)', db2: 'FLOAT(11)', @@ -1180,6 +1273,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('FLOAT(11).ZEROFILL.UNSIGNED', DataTypes.FLOAT(11).ZEROFILL.UNSIGNED, { default: 'FLOAT(11) UNSIGNED ZEROFILL', + oracle: 'BINARY_FLOAT', sqlite: 'FLOAT UNSIGNED ZEROFILL(11)', postgres: 'FLOAT(11)', db2: 'FLOAT(11)', @@ -1188,6 +1282,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('FLOAT(11, 12)', DataTypes.FLOAT(11, 12), { default: 'FLOAT(11,12)', + oracle: 'BINARY_FLOAT', postgres: 'FLOAT', db2: 'FLOAT', mssql: 'FLOAT' @@ -1195,6 +1290,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('FLOAT(11, 12).UNSIGNED', DataTypes.FLOAT(11, 12).UNSIGNED, { default: 'FLOAT(11,12) UNSIGNED', + oracle: 'BINARY_FLOAT', sqlite: 'FLOAT UNSIGNED(11,12)', postgres: 'FLOAT', db2: 'FLOAT', @@ -1203,6 +1299,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('FLOAT({ length: 11, decimals: 12 }).UNSIGNED', DataTypes.FLOAT({ length: 11, decimals: 12 }).UNSIGNED, { default: 'FLOAT(11,12) UNSIGNED', + oracle: 'BINARY_FLOAT', sqlite: 'FLOAT UNSIGNED(11,12)', postgres: 'FLOAT', db2: 'FLOAT', @@ -1211,6 +1308,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('FLOAT(11, 12).UNSIGNED.ZEROFILL', DataTypes.FLOAT(11, 12).UNSIGNED.ZEROFILL, { default: 'FLOAT(11,12) UNSIGNED ZEROFILL', + oracle: 'BINARY_FLOAT', sqlite: 'FLOAT UNSIGNED ZEROFILL(11,12)', postgres: 'FLOAT', db2: 'FLOAT', @@ -1219,6 +1317,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('FLOAT(11, 12).ZEROFILL', DataTypes.FLOAT(11, 12).ZEROFILL, { default: 'FLOAT(11,12) ZEROFILL', + oracle: 'BINARY_FLOAT', sqlite: 'FLOAT ZEROFILL(11,12)', postgres: 'FLOAT', db2: 'FLOAT', @@ -1227,6 +1326,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('FLOAT(11, 12).ZEROFILL.UNSIGNED', DataTypes.FLOAT(11, 12).ZEROFILL.UNSIGNED, { default: 'FLOAT(11,12) UNSIGNED ZEROFILL', + oracle: 'BINARY_FLOAT', sqlite: 'FLOAT UNSIGNED ZEROFILL(11,12)', postgres: 'FLOAT', db2: 'FLOAT', @@ -1256,51 +1356,61 @@ describe(Support.getTestDialectTeaser('SQL'), () => { if (current.dialect.supports.NUMERIC) { testsql('NUMERIC', DataTypes.NUMERIC, { - default: 'DECIMAL' + default: 'DECIMAL', + oracle: 'NUMBER' }); testsql('NUMERIC(15,5)', DataTypes.NUMERIC(15, 5), { - default: 'DECIMAL(15,5)' + default: 'DECIMAL(15,5)', + oracle: 'NUMBER' }); } describe('DECIMAL', () => { testsql('DECIMAL', DataTypes.DECIMAL, { - default: 'DECIMAL' + default: 'DECIMAL', + oracle: 'NUMBER' }); testsql('DECIMAL(10, 2)', DataTypes.DECIMAL(10, 2), { - default: 'DECIMAL(10,2)' + default: 'DECIMAL(10,2)', + oracle: 'NUMBER' }); testsql('DECIMAL({ precision: 10, scale: 2 })', DataTypes.DECIMAL({ precision: 10, scale: 2 }), { - default: 'DECIMAL(10,2)' + default: 'DECIMAL(10,2)', + oracle: 'NUMBER' }); testsql('DECIMAL(10)', DataTypes.DECIMAL(10), { - default: 'DECIMAL(10)' + default: 'DECIMAL(10)', + oracle: 'NUMBER' }); testsql('DECIMAL({ precision: 10 })', DataTypes.DECIMAL({ precision: 10 }), { - default: 'DECIMAL(10)' + default: 'DECIMAL(10)', + oracle: 'NUMBER' }); testsql('DECIMAL.UNSIGNED', DataTypes.DECIMAL.UNSIGNED, { mariadb: 'DECIMAL UNSIGNED', mysql: 'DECIMAL UNSIGNED', - default: 'DECIMAL' + default: 'DECIMAL', + oracle: 'NUMBER' }); testsql('DECIMAL.UNSIGNED.ZEROFILL', DataTypes.DECIMAL.UNSIGNED.ZEROFILL, { mariadb: 'DECIMAL UNSIGNED ZEROFILL', mysql: 'DECIMAL UNSIGNED ZEROFILL', - default: 'DECIMAL' + default: 'DECIMAL', + oracle: 'NUMBER' }); testsql('DECIMAL({ precision: 10, scale: 2 }).UNSIGNED', DataTypes.DECIMAL({ precision: 10, scale: 2 }).UNSIGNED, { mariadb: 'DECIMAL(10,2) UNSIGNED', mysql: 'DECIMAL(10,2) UNSIGNED', - default: 'DECIMAL(10,2)' + default: 'DECIMAL(10,2)', + oracle: 'NUMBER' }); describe('validate', () => { @@ -1368,6 +1478,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('BLOB("tiny")', DataTypes.BLOB('tiny'), { default: 'TINYBLOB', + oracle: 'BLOB', mssql: 'VARBINARY(256)', db2: 'BLOB(255)', postgres: 'BYTEA' @@ -1375,6 +1486,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('BLOB("medium")', DataTypes.BLOB('medium'), { default: 'MEDIUMBLOB', + oracle: 'BLOB', mssql: 'VARBINARY(MAX)', db2: 'BLOB(16M)', postgres: 'BYTEA' @@ -1382,6 +1494,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('BLOB({ length: "medium" })', DataTypes.BLOB({ length: 'medium' }), { default: 'MEDIUMBLOB', + oracle: 'BLOB', mssql: 'VARBINARY(MAX)', db2: 'BLOB(16M)', postgres: 'BYTEA' @@ -1389,6 +1502,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { testsql('BLOB("long")', DataTypes.BLOB('long'), { default: 'LONGBLOB', + oracle: 'BLOB', mssql: 'VARBINARY(MAX)', db2: 'BLOB(2G)', postgres: 'BYTEA' diff --git a/test/unit/sql/delete.test.js b/test/unit/sql/delete.test.js index 2e0d341108a8..ec34bd5236c6 100644 --- a/test/unit/sql/delete.test.js +++ b/test/unit/sql/delete.test.js @@ -40,6 +40,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: 'TRUNCATE `public.test_users`', db2: 'TRUNCATE TABLE "public"."test_users" IMMEDIATE', sqlite: 'DELETE FROM `public.test_users`', + oracle: 'TRUNCATE TABLE "public"."test_users"', snowflake: 'TRUNCATE "public"."test_users"' } ); @@ -69,6 +70,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: 'TRUNCATE `public.test_users`', db2: 'TRUNCATE TABLE "public"."test_users" IMMEDIATE', sqlite: 'DELETE FROM `public.test_users`; DELETE FROM `sqlite_sequence` WHERE `name` = \'public.test_users\';', + oracle: 'TRUNCATE TABLE "public"."test_users"', snowflake: 'TRUNCATE "public"."test_users"' } ); @@ -97,6 +99,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: "DELETE FROM `public.test_users` WHERE `name` = 'foo'", db2: 'DELETE FROM "public"."test_users" WHERE "name" = \'foo\'', mssql: "DELETE FROM [public].[test_users] WHERE [name] = N'foo'; SELECT @@ROWCOUNT AS AFFECTEDROWS;", + oracle: 'DELETE FROM "public"."test_users" WHERE "name" = \'foo\'', snowflake: 'DELETE FROM "public"."test_users" WHERE "name" = \'foo\';' } ); @@ -125,6 +128,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mssql: "DELETE TOP(10) FROM [public].[test_users] WHERE [name] = N'foo'';DROP TABLE mySchema.myTable;'; SELECT @@ROWCOUNT AS AFFECTEDROWS;", db2: "DELETE FROM \"public\".\"test_users\" WHERE \"name\" = 'foo'';DROP TABLE mySchema.myTable;' FETCH NEXT 10 ROWS ONLY", snowflake: 'DELETE FROM "public"."test_users" WHERE "id" IN (SELECT "id" FROM "public"."test_users" WHERE "name" = \'foo\'\';DROP TABLE mySchema.myTable;\' LIMIT 10);', + oracle: 'DELETE FROM "public"."test_users" WHERE rowid IN (SELECT rowid FROM "public"."test_users" WHERE rownum <= 10 AND "name" = \'foo\'\';DROP TABLE mySchema.myTable;\')', default: "DELETE FROM [public.test_users] WHERE `name` = 'foo\\';DROP TABLE mySchema.myTable;' LIMIT 10" } ); @@ -159,6 +163,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: "DELETE FROM `public.test_users` WHERE rowid IN (SELECT rowid FROM `public.test_users` WHERE `name` = 'foo'';DROP TABLE mySchema.myTable;' LIMIT 10)", mssql: "DELETE TOP(10) FROM [public].[test_users] WHERE [name] = N'foo'';DROP TABLE mySchema.myTable;'; SELECT @@ROWCOUNT AS AFFECTEDROWS;", db2: "DELETE FROM \"public\".\"test_users\" WHERE \"name\" = 'foo'';DROP TABLE mySchema.myTable;' FETCH NEXT 10 ROWS ONLY", + oracle: 'DELETE FROM "public"."test_users" WHERE rowid IN (SELECT rowid FROM "public"."test_users" WHERE rownum <= 10 AND "name" = \'foo\'\';DROP TABLE mySchema.myTable;\')', snowflake: new Error('Cannot LIMIT delete without a model.'), default: "DELETE FROM [public.test_users] WHERE `name` = 'foo\\';DROP TABLE mySchema.myTable;' LIMIT 10" } diff --git a/test/unit/sql/generateJoin.test.js b/test/unit/sql/generateJoin.test.js index 5283d048ba14..6ce3f18e1d6c 100644 --- a/test/unit/sql/generateJoin.test.js +++ b/test/unit/sql/generateJoin.test.js @@ -99,6 +99,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] }, { + oracle: 'LEFT OUTER JOIN "company" "Company" ON "User"."company_id" = "Company"."id"', default: 'LEFT OUTER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id]' } ); @@ -118,6 +119,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { { default: 'INNER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id] OR [Company].[public] = true', sqlite: 'INNER JOIN `company` AS `Company` ON `User`.`company_id` = `Company`.`id` OR `Company`.`public` = 1', + oracle: 'INNER JOIN "company" "Company" ON "User"."company_id" = "Company"."id" OR "Company"."public" = \'1\'', mssql: 'INNER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id] OR [Company].[public] = 1' } ); @@ -137,6 +139,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] }, { + oracle: 'LEFT OUTER JOIN "company" "Professionals->Company" ON "Professionals"."company_id" = "Professionals->Company"."id"', default: 'LEFT OUTER JOIN [company] AS [Professionals->Company] ON [Professionals].[company_id] = [Professionals->Company].[id]' } ); @@ -151,6 +154,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] }, { + oracle: 'LEFT OUTER JOIN "company" "Company" ON "User"."companyId" = "Company"."id"', default: 'LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]' } ); @@ -168,7 +172,9 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id] AND [Company].[name] = 'ABC'", + oracle: 'LEFT OUTER JOIN "company" "Company" ON "User"."companyId" = "Company"."id" AND "Company"."name" = \'ABC\'', mssql: "LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id] AND [Company].[name] = N'ABC'" + } ); @@ -184,6 +190,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] }, { + oracle: 'RIGHT OUTER JOIN "company" "Company" ON "User"."companyId" = "Company"."id"', default: `${current.dialect.supports['RIGHT JOIN'] ? 'RIGHT' : 'LEFT'} OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]` } ); @@ -203,6 +210,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { + oracle: 'LEFT OUTER JOIN "user" "Company->Owner" ON "Company"."owner_id" = "Company->Owner"."id_user"', default: 'LEFT OUTER JOIN [user] AS [Company->Owner] ON [Company].[owner_id] = [Company->Owner].[id_user]' } ); @@ -224,7 +232,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { } ] }, - { default: 'LEFT OUTER JOIN [profession] AS [Company->Owner->Profession] ON [Company->Owner].[professionId] = [Company->Owner->Profession].[id]' } + { + oracle: 'LEFT OUTER JOIN "profession" "Company->Owner->Profession" ON "Company->Owner"."professionId" = "Company->Owner->Profession"."id"', + default: 'LEFT OUTER JOIN [profession] AS [Company->Owner->Profession] ON [Company->Owner].[professionId] = [Company->Owner->Profession].[id]' + } ); testsql( @@ -242,7 +253,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { } ] }, - { default: 'LEFT OUTER JOIN [user] AS [Company->Owner] ON [Company].[owner_id] = [Company->Owner].[id_user]' } + { + oracle: 'LEFT OUTER JOIN "user" "Company->Owner" ON "Company"."owner_id" = "Company->Owner"."id_user"', + default: 'LEFT OUTER JOIN [user] AS [Company->Owner] ON [Company].[owner_id] = [Company->Owner].[id_user]' + } ); testsql( @@ -255,6 +269,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] }, { + oracle: 'INNER JOIN "company" "Company" ON "User"."companyId" = "Company"."id"', default: 'INNER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]' } ); @@ -271,7 +286,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { User.Tasks ] }, - { default: 'LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id_user] = [Tasks].[user_id]' } + { + oracle: 'LEFT OUTER JOIN "task" "Tasks" ON "User"."id_user" = "Tasks"."user_id"', + default: 'LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id_user] = [Tasks].[user_id]' + } ); testsql( @@ -285,6 +303,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { // The primary key of the main model will be aliased because it's coming from a subquery that the :M join is not a part of + oracle: 'LEFT OUTER JOIN "task" "Tasks" ON "User"."id" = "Tasks"."user_id"', default: 'LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id] = [Tasks].[user_id]' } ); @@ -303,7 +322,11 @@ describe(Support.getTestDialectTeaser('SQL'), () => { } } ] - }, { default: 'LEFT OUTER JOIN [task] AS [Tasks] ON ([User].[id_user] = [Tasks].[user_id] OR [Tasks].[user_id] = 2)' } + }, + { + oracle: 'LEFT OUTER JOIN "task" "Tasks" ON ("User"."id_user" = "Tasks"."user_id" OR "Tasks"."user_id" = 2)', + default: 'LEFT OUTER JOIN [task] AS [Tasks] ON ([User].[id_user] = [Tasks].[user_id] OR [Tasks].[user_id] = 2)' + } ); testsql( @@ -316,7 +339,11 @@ describe(Support.getTestDialectTeaser('SQL'), () => { on: { 'user_id': { [Op.col]: 'User.alternative_id' } } } ] - }, { default: 'LEFT OUTER JOIN [task] AS [Tasks] ON [Tasks].[user_id] = [User].[alternative_id]' } + }, + { + oracle: 'LEFT OUTER JOIN "task" "Tasks" ON "Tasks"."user_id" = "User"."alternative_id"', + default: 'LEFT OUTER JOIN [task] AS [Tasks] ON [Tasks].[user_id] = [User].[alternative_id]' + } ); testsql( @@ -343,6 +370,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { + oracle: 'LEFT OUTER JOIN "user" "Company->Owner" ON ("Company"."owner_id" = "Company->Owner"."id_user" OR "Company->Owner"."id_user" = 2)', default: 'LEFT OUTER JOIN [user] AS [Company->Owner] ON ([Company].[owner_id] = [Company->Owner].[id_user] OR [Company->Owner].[id_user] = 2)' } ); diff --git a/test/unit/sql/group.test.js b/test/unit/sql/group.test.js index 2e51466bfc1c..a6d1407abd3a 100644 --- a/test/unit/sql/group.test.js +++ b/test/unit/sql/group.test.js @@ -41,6 +41,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'SELECT * FROM "Users" AS "User" GROUP BY "name";', db2: 'SELECT * FROM "Users" AS "User" GROUP BY "name";', mssql: 'SELECT * FROM [Users] AS [User] GROUP BY [name];', + oracle: 'SELECT * FROM "Users" "User" GROUP BY "name";', snowflake: 'SELECT * FROM "Users" AS "User" GROUP BY "name";' }); @@ -52,6 +53,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'SELECT * FROM "Users" AS "User";', db2: 'SELECT * FROM "Users" AS "User";', mssql: 'SELECT * FROM [Users] AS [User];', + oracle: 'SELECT * FROM "Users" "User";', snowflake: 'SELECT * FROM "Users" AS "User";' }); }); diff --git a/test/unit/sql/index.test.js b/test/unit/sql/index.test.js index 03371188ade7..8ebb7904677b 100644 --- a/test/unit/sql/index.test.js +++ b/test/unit/sql/index.test.js @@ -54,6 +54,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mssql: 'CREATE FULLTEXT INDEX [user_field_c] ON [User] ([fieldC])', postgres: 'CREATE INDEX CONCURRENTLY "user_field_c" ON "User" ("fieldC")', mariadb: 'ALTER TABLE `User` ADD FULLTEXT INDEX `user_field_c` (`fieldC`)', + oracle: 'CREATE INDEX "user_field_c" ON "User" ("fieldC")', mysql: 'ALTER TABLE `User` ADD FULLTEXT INDEX `user_field_c` (`fieldC`)' }); @@ -68,6 +69,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'CREATE UNIQUE INDEX "a_b_uniq" ON "User" ("fieldB", "fieldA" DESC)', postgres: 'CREATE UNIQUE INDEX "a_b_uniq" ON "User" USING BTREE ("fieldB", "fieldA" COLLATE "en_US" DESC)', mariadb: 'ALTER TABLE `User` ADD UNIQUE INDEX `a_b_uniq` USING BTREE (`fieldB`, `fieldA`(5) DESC) WITH PARSER foo', + oracle: 'CREATE UNIQUE INDEX "a_b_uniq" ON "User" ("fieldB", "fieldA" DESC)', mysql: 'ALTER TABLE `User` ADD UNIQUE INDEX `a_b_uniq` USING BTREE (`fieldB`, `fieldA`(5) DESC) WITH PARSER foo' }); }); @@ -78,6 +80,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mssql: 'CREATE INDEX [table_column] ON [table] ([column] DESC)', db2: 'CREATE INDEX "table_column" ON "table" ("column" DESC)', mariadb: 'ALTER TABLE `table` ADD INDEX `table_column` (`column`(5) DESC)', + oracle: 'CREATE INDEX "table_column" ON "table" ("column" DESC)', mysql: 'ALTER TABLE `table` ADD INDEX `table_column` (`column`(5) DESC)' }); }); @@ -250,6 +253,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: 'DROP INDEX `table_column1_column2` ON `table`', mssql: 'DROP INDEX [table_column1_column2] ON [table]', db2: 'DROP INDEX "table_column1_column2"', + oracle: 'DROP INDEX "table_column1_column2"', default: 'DROP INDEX IF EXISTS [table_column1_column2]' }); }); diff --git a/test/unit/sql/insert.test.js b/test/unit/sql/insert.test.js index ea5fe6e31576..4231a1affd0e 100644 --- a/test/unit/sql/insert.test.js +++ b/test/unit/sql/insert.test.js @@ -32,6 +32,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'INSERT INTO "users" ("user_name") VALUES ($1) RETURNING "id","user_name";', db2: 'SELECT * FROM FINAL TABLE(INSERT INTO "users" ("user_name") VALUES ($1));', snowflake: 'INSERT INTO "users" ("user_name") VALUES ($1);', + oracle: 'INSERT INTO "users" ("user_name") VALUES (:1) RETURNING "id","user_name" INTO :2,:3;', default: 'INSERT INTO `users` (`user_name`) VALUES ($1);' }, bind: ['triggertest'] @@ -55,6 +56,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'SELECT * FROM FINAL TABLE(INSERT INTO "ms" ("id") VALUES ($1));', postgres: 'INSERT INTO "ms" ("id") VALUES ($1);', snowflake: 'INSERT INTO "ms" ("id") VALUES ($1);', + oracle: 'INSERT INTO "ms" ("id") VALUES (:1);', default: 'INSERT INTO `ms` (`id`) VALUES ($1);' }, bind: [0] @@ -82,6 +84,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'INSERT INTO "users" ("date") VALUES ($1);', db2: 'SELECT * FROM FINAL TABLE(INSERT INTO "users" ("date") VALUES ($1));', snowflake: 'INSERT INTO "users" ("date") VALUES ($1);', + oracle: 'INSERT INTO "users" ("date") VALUES (:1);', mssql: 'INSERT INTO [users] ([date]) VALUES ($1);', default: 'INSERT INTO `users` (`date`) VALUES ($1);' }, @@ -91,6 +94,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: ['2015-01-20 01:00:00'], snowflake: ['2015-01-20 01:00:00'], mariadb: ['2015-01-20 01:00:00.000'], + oracle: [new Date(Date.UTC(2015, 0, 20))], default: ['2015-01-20 01:00:00.000 +01:00'] } }); @@ -116,11 +120,13 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'SELECT * FROM FINAL TABLE(INSERT INTO "users" ("date") VALUES ($1));', snowflake: 'INSERT INTO "users" ("date") VALUES ($1);', mssql: 'INSERT INTO [users] ([date]) VALUES ($1);', + oracle: 'INSERT INTO "users" ("date") VALUES (:1);', default: 'INSERT INTO `users` (`date`) VALUES ($1);' }, bind: { sqlite: ['2015-01-20 01:02:03.089 +00:00'], mariadb: ['2015-01-20 02:02:03.089'], + oracle: [new Date(Date.UTC(2015, 0, 20, 1, 2, 3, 89))], mysql: ['2015-01-20 02:02:03.089'], db2: ['2015-01-20 02:02:03.089'], snowflake: ['2015-01-20 02:02:03.089'], @@ -148,6 +154,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'SELECT * FROM FINAL TABLE(INSERT INTO "users" ("user_name") VALUES ($1));', snowflake: 'INSERT INTO "users" ("user_name") VALUES ($1);', mssql: 'INSERT INTO [users] ([user_name]) VALUES ($1);', + oracle: 'INSERT INTO "users" ("user_name") VALUES (:1);', default: 'INSERT INTO `users` (`user_name`) VALUES ($1);' }, bind: { @@ -189,6 +196,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { { default: 'INSERT INTO `users` (`user_name`,`pass_word`) VALUES (\'testuser\',\'12345\');', snowflake: 'INSERT INTO "users" ("user_name","pass_word") VALUES (\'testuser\',\'12345\');', + oracle: 'INSERT INTO "users" ("user_name","pass_word") VALUES (:1,:2)', postgres: 'INSERT INTO "users" ("user_name","pass_word") VALUES (\'testuser\',\'12345\') ON CONFLICT ("user_name") DO UPDATE SET "user_name"=EXCLUDED."user_name","pass_word"=EXCLUDED."pass_word","updated_at"=EXCLUDED."updated_at";', mssql: 'INSERT INTO [users] ([user_name],[pass_word]) VALUES (N\'testuser\',N\'12345\');', db2: 'INSERT INTO "users" ("user_name","pass_word") VALUES (\'testuser\',\'12345\');', @@ -198,7 +206,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }); }); - it('allow bulk insert primary key with 0', () => { + // Oracle dialect doesn't support mix of null and non-null in auto-increment column + (current.dialect.name !== 'oracle' ? it : it.skip)('allow bulk insert primary key with 0', () => { const M = Support.sequelize.define('m', { id: { type: DataTypes.INTEGER, diff --git a/test/unit/sql/json.test.js b/test/unit/sql/json.test.js index 1e874a411ad0..8aa9771edf7a 100644 --- a/test/unit/sql/json.test.js +++ b/test/unit/sql/json.test.js @@ -84,6 +84,7 @@ if (current.dialect.supports.JSON) { postgres: '("id"#>>\'{}\') = \'1\'', sqlite: "json_extract(`id`,'$') = '1'", mariadb: "json_unquote(json_extract(`id`,'$')) = '1'", + oracle: 'json_value("id",\'$\') = \'1\'', mysql: "json_unquote(json_extract(`id`,'$')) = '1'" }); }); @@ -93,6 +94,7 @@ if (current.dialect.supports.JSON) { postgres: '("profile"#>>\'{id}\') = \'1\'', sqlite: "json_extract(`profile`,'$.id') = '1'", mariadb: "json_unquote(json_extract(`profile`,'$.id')) = '1'", + oracle: 'json_value("profile",\'$."id"\') = \'1\'', mysql: "json_unquote(json_extract(`profile`,'$.\\\"id\\\"')) = '1'" }); }); @@ -102,6 +104,7 @@ if (current.dialect.supports.JSON) { postgres: '("property"#>>\'{value}\') = \'1\' AND ("another"#>>\'{value}\') = \'string\'', sqlite: "json_extract(`property`,'$.value') = '1' AND json_extract(`another`,'$.value') = 'string'", mariadb: "json_unquote(json_extract(`property`,'$.value')) = '1' AND json_unquote(json_extract(`another`,'$.value')) = 'string'", + oracle: 'json_value("property",\'$."value"\') = \'1\' AND json_value("another",\'$."value"\') = \'string\'', mysql: "json_unquote(json_extract(`property`,'$.\\\"value\\\"')) = '1' AND json_unquote(json_extract(`another`,'$.\\\"value\\\"')) = 'string'" }); }); @@ -111,6 +114,7 @@ if (current.dialect.supports.JSON) { postgres: '("property"#>>\'{0,0}\') = \'4\' AND ("property"#>>\'{0,1}\') = \'6\' AND ("property"#>>\'{1,0}\') = \'8\'', sqlite: "json_extract(`property`,'$[0][0]') = '4' AND json_extract(`property`,'$[0][1]') = '6' AND json_extract(`property`,'$[1][0]') = '8'", mariadb: "json_unquote(json_extract(`property`,'$[0][0]')) = '4' AND json_unquote(json_extract(`property`,'$[0][1]')) = '6' AND json_unquote(json_extract(`property`,'$[1][0]')) = '8'", + oracle: 'json_value("property",\'$[0][0]\') = \'4\' AND json_value("property",\'$[0][1]\') = \'6\' AND json_value("property",\'$[1][0]\') = \'8\'', mysql: "json_unquote(json_extract(`property`,'$[0][0]')) = '4' AND json_unquote(json_extract(`property`,'$[0][1]')) = '6' AND json_unquote(json_extract(`property`,'$[1][0]')) = '8'" }); }); @@ -120,6 +124,7 @@ if (current.dialect.supports.JSON) { postgres: '("profile"#>>\'{id}\') = \'1\'', sqlite: "json_extract(`profile`,'$.id') = '1'", mariadb: "json_unquote(json_extract(`profile`,'$.id')) = '1'", + oracle: 'json_value("profile",\'$."id"\') = \'1\'', mysql: "json_unquote(json_extract(`profile`,'$.\\\"id\\\"')) = '1'" }); }); @@ -129,6 +134,7 @@ if (current.dialect.supports.JSON) { postgres: '("profile"#>>\'{id,0,1}\') = \'1\'', sqlite: "json_extract(`profile`,'$.id[0][1]') = '1'", mariadb: "json_unquote(json_extract(`profile`,'$.id[0][1]')) = '1'", + oracle: 'json_value("profile",\'$."id"[0][1]\') = \'1\'', mysql: "json_unquote(json_extract(`profile`,'$.\\\"id\\\"[0][1]')) = '1'" }); }); @@ -138,6 +144,7 @@ if (current.dialect.supports.JSON) { postgres: '("json"#>>\'{}\') = \'{}\'', sqlite: "json_extract(`json`,'$') = '{}'", mariadb: "json_unquote(json_extract(`json`,'$')) = '{}'", + oracle: 'json_value("json",\'$\') = \'{}\'', mysql: "json_unquote(json_extract(`json`,'$')) = '{}'" }); }); @@ -159,7 +166,9 @@ if (current.dialect.supports.JSON) { }); it('nested json functions', () => { - expectsql(sql.handleSequelizeMethod(Sequelize.json('json_extract(json_object(\'{"profile":null}\'), "profile")')), { + const rawJSON = current.dialect.name === 'oracle' ? 'json_value(json_object(\'{"profile":null}\'), "profile")' : 'json_extract(json_object(\'{"profile":null}\'), "profile")'; + expectsql(sql.handleSequelizeMethod(Sequelize.json(rawJSON)), { + oracle: 'json_value(json_object(\'{"profile":null}\'), "profile")', default: 'json_extract(json_object(\'{"profile":null}\'), "profile")' }); }); diff --git a/test/unit/sql/offset-limit.test.js b/test/unit/sql/offset-limit.test.js index a81238eb6254..edb705b37b53 100644 --- a/test/unit/sql/offset-limit.test.js +++ b/test/unit/sql/offset-limit.test.js @@ -30,6 +30,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { default: ' LIMIT 10', db2: ' FETCH NEXT 10 ROWS ONLY', + oracle: ' ORDER BY "tableRef"."id" OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', mssql: ' ORDER BY [tableRef].[id] OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY' }); @@ -41,6 +42,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { default: ' LIMIT 10', db2: ' FETCH NEXT 10 ROWS ONLY', + oracle: ' OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', mssql: ' OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY' }); @@ -55,6 +57,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { snowflake: ' LIMIT 10 OFFSET 20', postgres: ' LIMIT 10 OFFSET 20', db2: ' OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY', + oracle: ' OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY', mssql: ' OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY' }); @@ -69,6 +72,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { snowflake: " LIMIT ''';DELETE FROM user'", mysql: " LIMIT '\\';DELETE FROM user'", db2: " FETCH NEXT ''';DELETE FROM user' ROWS ONLY", + oracle: " OFFSET 0 ROWS FETCH NEXT ''';DELETE FROM user' ROWS ONLY", mssql: " OFFSET 0 ROWS FETCH NEXT N''';DELETE FROM user' ROWS ONLY" }); @@ -85,6 +89,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { snowflake: " LIMIT 10 OFFSET ''';DELETE FROM user'", mysql: " LIMIT '\\';DELETE FROM user', 10", db2: ' FETCH NEXT 10 ROWS ONLY', + oracle: " OFFSET ''';DELETE FROM user' ROWS FETCH NEXT 10 ROWS ONLY", mssql: " OFFSET N''';DELETE FROM user' ROWS FETCH NEXT 10 ROWS ONLY" }); @@ -95,6 +100,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { db2: ' FETCH NEXT 10 ROWS ONLY', default: ' LIMIT 10', + oracle: ' OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', mssql: ' ORDER BY [tableRef].[id] OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY' }); }); diff --git a/test/unit/sql/order.test.js b/test/unit/sql/order.test.js index 3f399a3ad566..e3beee45fa84 100644 --- a/test/unit/sql/order.test.js +++ b/test/unit/sql/order.test.js @@ -341,6 +341,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] }, { default: 'SELECT [Subtask].[id], [Subtask].[name], [Subtask].[createdAt], [Task].[id] AS [Task.id], [Task].[name] AS [Task.name], [Task].[created_at] AS [Task.createdAt], [Task->Project].[id] AS [Task.Project.id], [Task->Project].[name] AS [Task.Project.name], [Task->Project].[created_at] AS [Task.Project.createdAt] FROM [subtask] AS [Subtask] INNER JOIN [task] AS [Task] ON [Subtask].[task_id] = [Task].[id] INNER JOIN [project] AS [Task->Project] ON [Task].[project_id] = [Task->Project].[id] ORDER BY [Task->Project].[created_at] ASC, [Task->Project].[created_at], [Task].[created_at] ASC, [Task].[created_at], [Task->Project].[created_at] ASC, [Task->Project].[created_at], [Task].[created_at] ASC, [Task].[created_at], [Task->Project].[created_at] ASC, [Task->Project].[created_at], [Task].[created_at] ASC, [Task].[created_at], [Task->Project].[created_at] ASC, [Task->Project].[created_at], [Task].[created_at] ASC, [Task].[created_at], [Subtask].[created_at] ASC, [Subtask].[created_at], [Subtask].[created_at];', + oracle: 'SELECT "Subtask"."id", "Subtask"."name", "Subtask"."createdAt", "Task"."id" AS "Task.id", "Task"."name" AS "Task.name", "Task"."created_at" AS "Task.createdAt", "Task->Project"."id" AS "Task.Project.id", "Task->Project"."name" AS "Task.Project.name", "Task->Project"."created_at" AS "Task.Project.createdAt" FROM "subtask" "Subtask" INNER JOIN "task" "Task" ON "Subtask"."task_id" = "Task"."id" INNER JOIN "project" "Task->Project" ON "Task"."project_id" = "Task->Project"."id" ORDER BY "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Subtask"."created_at" ASC, "Subtask"."created_at", "Subtask"."created_at";', postgres: 'SELECT "Subtask"."id", "Subtask"."name", "Subtask"."createdAt", "Task"."id" AS "Task.id", "Task"."name" AS "Task.name", "Task"."created_at" AS "Task.createdAt", "Task->Project"."id" AS "Task.Project.id", "Task->Project"."name" AS "Task.Project.name", "Task->Project"."created_at" AS "Task.Project.createdAt" FROM "subtask" AS "Subtask" INNER JOIN "task" AS "Task" ON "Subtask"."task_id" = "Task"."id" INNER JOIN "project" AS "Task->Project" ON "Task"."project_id" = "Task->Project"."id" ORDER BY "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Subtask"."created_at" ASC, "Subtask"."created_at", "Subtask"."created_at";' }); @@ -357,6 +358,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: 'SELECT `id`, `name` FROM `subtask` AS `Subtask` ORDER BY RAND();', postgres: 'SELECT "id", "name" FROM "subtask" AS "Subtask" ORDER BY RANDOM();', snowflake: 'SELECT "id", "name" FROM "subtask" AS "Subtask" ORDER BY RANDOM();', + oracle: 'SELECT "id", "name" FROM "subtask" "Subtask" ORDER BY RAND();', sqlite: 'SELECT `id`, `name` FROM `subtask` AS `Subtask` ORDER BY RANDOM();' }); diff --git a/test/unit/sql/remove-column.test.js b/test/unit/sql/remove-column.test.js index fc4f3fa6ee52..e825d4766c11 100644 --- a/test/unit/sql/remove-column.test.js +++ b/test/unit/sql/remove-column.test.js @@ -20,6 +20,7 @@ if (current.dialect.name !== 'sqlite') { mariadb: 'ALTER TABLE `archive`.`user` DROP `email`;', mysql: 'ALTER TABLE `archive.user` DROP `email`;', postgres: 'ALTER TABLE "archive"."user" DROP COLUMN "email";', + oracle: 'ALTER TABLE "archive"."user" DROP COLUMN "email";', snowflake: 'ALTER TABLE "archive"."user" DROP "email";' }); }); diff --git a/test/unit/sql/select.test.js b/test/unit/sql/select.test.js index 544d8d5cf6be..290aae12084a 100644 --- a/test/unit/sql/select.test.js +++ b/test/unit/sql/select.test.js @@ -48,6 +48,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { default: "SELECT [email], [first_name] AS [firstName] FROM [User] WHERE [User].[email] = 'jon.snow@gmail.com' ORDER BY [email] DESC LIMIT 10;", db2: 'SELECT "email", "first_name" AS "firstName" FROM "User" WHERE "User"."email" = \'jon.snow@gmail.com\' ORDER BY "email" DESC FETCH NEXT 10 ROWS ONLY;', + oracle: 'SELECT "email", "first_name" AS "firstName" FROM "User" WHERE "User"."email" = \'jon.snow@gmail.com\' ORDER BY "email" DESC OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY;', mssql: "SELECT [email], [first_name] AS [firstName] FROM [User] WHERE [User].[email] = N'jon.snow@gmail.com' ORDER BY [email] DESC OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY;" }); @@ -70,6 +71,12 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] } }, { + oracle: `SELECT "User".* FROM (${ + [ + `SELECT * FROM (SELECT "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "User" WHERE "User"."companyId" = 1 ORDER BY "last_name" ASC${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) sub`, + `SELECT * FROM (SELECT "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "User" WHERE "User"."companyId" = 5 ORDER BY "last_name" ASC${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) sub` + ].join(current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ') + }) "User" ORDER BY "last_name" ASC;`, default: `SELECT [User].* FROM (${ [ `SELECT * FROM (SELECT [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [User] WHERE [User].[companyId] = 1 ORDER BY [last_name] ASC${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub`, @@ -123,6 +130,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] } }, { + oracle: 'SELECT "user".* FROM (' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 1 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 5 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub)' + + ' "user" ORDER BY "subquery_order_0" ASC;', default: `SELECT [user].* FROM (${ [ `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[last_name] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 1 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub`, @@ -154,12 +165,16 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] } }, { + oracle: 'SELECT "user".* FROM (' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 1 AND "project_users"."status" = 1 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 5 AND "project_users"."status" = 1 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub)' + + ' "user" ORDER BY "subquery_order_0" ASC;', default: `SELECT [user].* FROM (${ [ `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[last_name] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 1 AND [project_users].[status] = 1 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub`, `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[last_name] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 5 AND [project_users].[status] = 1 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub` ].join(current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ') - }) AS [user] ORDER BY [subquery_order_0] ASC;` + }) AS [user] ORDER BY [subquery_order_0] ASC;` }); testsql({ @@ -185,12 +200,17 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] } }, { + oracle: 'SELECT "user".* FROM (' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."id_user" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 1 WHERE "user"."age" >= 21 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."id_user" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 5 WHERE "user"."age" >= 21 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub) ' + + '"user" ORDER BY "subquery_order_0" ASC;', default: `SELECT [user].* FROM (${ [ `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[id_user] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 1 WHERE [user].[age] >= 21 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub`, `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[id_user] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 5 WHERE [user].[age] >= 21 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub` ].join(current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ') }) AS [user] ORDER BY [subquery_order_0] ASC;` + }); }()); @@ -271,6 +291,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] } }, { + oracle: 'SELECT "user".*, "POSTS"."id" AS "POSTS.id", "POSTS"."title" AS "POSTS.title" FROM (' + + 'SELECT * FROM (SELECT "id_user" AS "id", "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "users" "user" WHERE "user"."companyId" = 1 ORDER BY "user"."last_name" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + + 'SELECT * FROM (SELECT "id_user" AS "id", "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "users" "user" WHERE "user"."companyId" = 5 ORDER BY "user"."last_name" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub) ' + + '"user" LEFT OUTER JOIN "post" "POSTS" ON "user"."id" = "POSTS"."user_id" ORDER BY "user"."last_name" ASC;', default: `SELECT [user].*, [POSTS].[id] AS [POSTS.id], [POSTS].[title] AS [POSTS.title] FROM (${ [ `SELECT * FROM (SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] AS [user] WHERE [user].[companyId] = 1 ORDER BY [user].[last_name] ASC${sql.addLimitAndOffset({ limit: 3, order: [['last_name', 'ASC']] })}) AS sub`, @@ -295,6 +319,9 @@ describe(Support.getTestDialectTeaser('SQL'), () => { hasMultiAssociation: true, //must be set only for mssql dialect here subQuery: true }, { + oracle: 'SELECT "user".*, "POSTS"."id" AS "POSTS.id", "POSTS"."title" AS "POSTS.title" FROM (' + + 'SELECT "user"."id_user" AS "id", "user"."email", "user"."first_name" AS "firstName", "user"."last_name" AS "lastName" FROM "users" "user" ORDER BY "user"."last_name" ASC OFFSET 10 ROWS FETCH NEXT 30 ROWS ONLY)' + + ' "user" LEFT OUTER JOIN "post" "POSTS" ON "user"."id_user" = "POSTS"."user_id" ORDER BY "user"."last_name" ASC;', default: `${'SELECT [user].*, [POSTS].[id] AS [POSTS.id], [POSTS].[title] AS [POSTS.title] FROM (' + 'SELECT [user].[id_user] AS [id], [user].[email], [user].[first_name] AS [firstName], [user].[last_name] AS [lastName] FROM [users] AS [user] ORDER BY [user].[last_name] ASC'}${ sql.addLimitAndOffset({ limit: 30, offset: 10, order: [['`user`.`last_name`', 'ASC']] }) @@ -319,6 +346,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { hasMultiAssociation: true, // must be set only for mssql dialect here subQuery: false }, { + oracle: 'SELECT "user"."id_user" AS "id", "user"."email", "user"."first_name" AS "firstName", "user"."last_name" AS "lastName", "POSTS"."id" AS "POSTS.id", "POSTS"."title" AS "POSTS.title" FROM "users" "user" LEFT OUTER JOIN "post" "POSTS" ON "user"."id_user" = "POSTS"."user_id" ORDER BY "user"."last_name" ASC OFFSET 10 ROWS FETCH NEXT 30 ROWS ONLY;', default: Support.minifySql(`SELECT [user].[id_user] AS [id], [user].[email], [user].[first_name] AS [firstName], [user].[last_name] AS [lastName], [POSTS].[id] AS [POSTS.id], [POSTS].[title] AS [POSTS.title] FROM [users] AS [user] LEFT OUTER JOIN [post] AS [POSTS] ON [user].[id_user] = [POSTS].[user_id] @@ -361,6 +389,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] } }, { + oracle: 'SELECT "user".*, "POSTS"."id" AS "POSTS.id", "POSTS"."title" AS "POSTS.title", "POSTS->COMMENTS"."id" AS "POSTS.COMMENTS.id", "POSTS->COMMENTS"."title" AS "POSTS.COMMENTS.title" FROM (' + + 'SELECT * FROM (SELECT "id_user" AS "id", "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "users" "user" WHERE "user"."companyId" = 1 ORDER BY "user"."last_name" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + + 'SELECT * FROM (SELECT "id_user" AS "id", "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "users" "user" WHERE "user"."companyId" = 5 ORDER BY "user"."last_name" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub)' + + ' "user" LEFT OUTER JOIN "post" "POSTS" ON "user"."id" = "POSTS"."user_id" LEFT OUTER JOIN "comment" "POSTS->COMMENTS" ON "POSTS"."id" = "POSTS->COMMENTS"."post_id" ORDER BY "user"."last_name" ASC;', default: `SELECT [user].*, [POSTS].[id] AS [POSTS.id], [POSTS].[title] AS [POSTS.title], [POSTS->COMMENTS].[id] AS [POSTS.COMMENTS.id], [POSTS->COMMENTS].[title] AS [POSTS.COMMENTS.title] FROM (${ [ `SELECT * FROM (SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] AS [user] WHERE [user].[companyId] = 1 ORDER BY [user].[last_name] ASC${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub`, @@ -398,6 +430,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }).include, model: User }, User), { + oracle: 'SELECT "User"."name", "User"."age", "Posts"."id" AS "Posts.id", "Posts"."title" AS "Posts.title" FROM "User" "User" LEFT OUTER JOIN "Post" "Posts" ON "User"."id" = "Posts"."user_id";', default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[title] AS [Posts.title] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];' }); }); @@ -431,6 +464,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }).include, model: User }, User), { + oracle: 'SELECT "User"."name", "User"."age", "Posts"."id" AS "Posts.id", "Posts"."title" AS "Posts.title" FROM "User" "User" RIGHT OUTER JOIN "Post" "Posts" ON "User"."id" = "Posts"."user_id";', default: `SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[title] AS [Posts.title] FROM [User] AS [User] ${current.dialect.supports['RIGHT JOIN'] ? 'RIGHT' : 'LEFT'} OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];` }); }); @@ -473,7 +507,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }).include, model: User }, User), { - default: `SELECT [user].[id_user], [user].[id], [projects].[id] AS [projects.id], [projects].[title] AS [projects.title], [projects].[createdAt] AS [projects.createdAt], [projects].[updatedAt] AS [projects.updatedAt], [projects->project_user].[user_id] AS [projects.project_user.userId], [projects->project_user].[project_id] AS [projects.project_user.projectId] FROM [User] AS [user] ${current.dialect.supports['RIGHT JOIN'] ? 'RIGHT' : 'LEFT'} OUTER JOIN ( [project_users] AS [projects->project_user] INNER JOIN [projects] AS [projects] ON [projects].[id] = [projects->project_user].[project_id]) ON [user].[id_user] = [projects->project_user].[user_id];` + default: `SELECT [user].[id_user], [user].[id], [projects].[id] AS [projects.id], [projects].[title] AS [projects.title], [projects].[createdAt] AS [projects.createdAt], [projects].[updatedAt] AS [projects.updatedAt], [projects->project_user].[user_id] AS [projects.project_user.userId], [projects->project_user].[project_id] AS [projects.project_user.projectId] FROM [User] AS [user] ${current.dialect.supports['RIGHT JOIN'] ? 'RIGHT' : 'LEFT'} OUTER JOIN ( [project_users] AS [projects->project_user] INNER JOIN [projects] AS [projects] ON [projects].[id] = [projects->project_user].[project_id]) ON [user].[id_user] = [projects->project_user].[user_id];`, + oracle: 'SELECT "user"."id_user", "user"."id", "projects"."id" AS "projects.id", "projects"."title" AS "projects.title", "projects"."createdAt" AS "projects.createdAt", "projects"."updatedAt" AS "projects.updatedAt", "projects->project_user"."user_id" AS "projects.project_user.userId", "projects->project_user"."project_id" AS "projects.project_user.projectId" FROM "User" "user" RIGHT OUTER JOIN ( "project_users" "projects->project_user" INNER JOIN "projects" "projects" ON "projects"."id" = "projects->project_user"."project_id") ON "user"."id_user" = "projects->project_user"."user_id";' }); }); @@ -510,6 +545,9 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }).include, subQuery: true }, User), { + oracle: 'SELECT "User".* FROM ' + + '(SELECT "User"."name", "User"."age", "User"."id" AS "id", "postaliasname"."id" AS "postaliasname.id", "postaliasname"."title" AS "postaliasname.title" FROM "User" "User" ' + + 'INNER JOIN "Post" "postaliasname" ON "User"."id" = "postaliasname"."user_id" WHERE ( SELECT "user_id" FROM "Post" "postaliasname" WHERE ("postaliasname"."user_id" = "User"."id") ORDER BY "postaliasname"."id" OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY ) IS NOT NULL) "User";', default: 'SELECT [User].* FROM ' + '(SELECT [User].[name], [User].[age], [User].[id] AS [id], [postaliasname].[id] AS [postaliasname.id], [postaliasname].[title] AS [postaliasname.title] FROM [User] AS [User] ' + 'INNER JOIN [Post] AS [postaliasname] ON [User].[id] = [postaliasname].[user_id] ' + @@ -534,6 +572,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }).include, subQuery: true }, User), { + oracle: 'SELECT "User".* FROM ' + + '(SELECT "User"."name", "User"."age", "User"."id" AS "id", "postaliasname"."id" AS "postaliasname.id", "postaliasname"."title" AS "postaliasname.title" FROM "User" "User" ' + + 'INNER JOIN "Post" "postaliasname" ON "User"."id" = "postaliasname"."user_id" WHERE "postaliasname"."title" = \'test\' AND ( SELECT "user_id" FROM "Post" "postaliasname" ' + + 'WHERE ("postaliasname"."user_id" = "User"."id") ORDER BY "postaliasname"."id" OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY ) IS NOT NULL) "User";', default: 'SELECT [User].* FROM ' + '(SELECT [User].[name], [User].[age], [User].[id] AS [id], [postaliasname].[id] AS [postaliasname.id], [postaliasname].[title] AS [postaliasname.title] FROM [User] AS [User] ' + 'INNER JOIN [Post] AS [postaliasname] ON [User].[id] = [postaliasname].[user_id] ' + @@ -586,6 +628,15 @@ describe(Support.getTestDialectTeaser('SQL'), () => { offset: 0, subQuery: true }, Company), { + oracle: 'SELECT "Company".* FROM (' + + 'SELECT "Company"."name", "Company"."public", "Company"."id" AS "id" FROM "Company" "Company" ' + + 'INNER JOIN "Users" "Users" ON "Company"."id" = "Users"."companyId" ' + + 'INNER JOIN "Professions" "Users->Profession" ON "Users"."professionId" = "Users->Profession"."id" ' + + 'WHERE ("Company"."scopeId" IN (42)) AND "Users->Profession"."name" = \'test\' AND ( ' + + 'SELECT "Users"."companyId" FROM "Users" "Users" ' + + 'INNER JOIN "Professions" "Profession" ON "Users"."professionId" = "Profession"."id" ' + + 'WHERE ("Users"."companyId" = "Company"."id") ORDER BY "Users"."id" OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY ) ' + + 'IS NOT NULL ORDER BY "Company"."id" OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY) "Company";', default: 'SELECT [Company].* FROM (' + 'SELECT [Company].[name], [Company].[public], [Company].[id] AS [id] FROM [Company] AS [Company] ' + 'INNER JOIN [Users] AS [Users] ON [Company].[id] = [Users].[companyId] ' + @@ -619,6 +670,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: 'SELECT `name`, `age`, `data` FROM `User` AS `User` WHERE `User`.`data` IN (X\'313233\');', sqlite: 'SELECT `name`, `age`, `data` FROM `User` AS `User` WHERE `User`.`data` IN (X\'313233\');', db2: "SELECT \"name\", \"age\", \"data\" FROM \"User\" AS \"User\" WHERE \"User\".\"data\" IN ('x''313233''');", + oracle: 'SELECT "name", "age", "data" FROM "User" "User" WHERE "User"."data" IN (\'313233\');', mssql: 'SELECT [name], [age], [data] FROM [User] AS [User] WHERE [User].[data] IN (0x313233);' }); }); @@ -718,8 +770,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }).include, model: User }, User), { - default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[* FROM User; DELETE FROM User;SELECT id] AS [Posts.* FROM User; DELETE FROM User;SELECT id] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];' - }); + oracle: 'SELECT "User"."name", "User"."age", "Posts"."id" AS "Posts.id", "Posts"."* FROM User; DELETE FROM User;SELECT id" AS "Posts.* FROM User; DELETE FROM User;SELECT id" FROM "User" "User" LEFT OUTER JOIN "Post" "Posts" ON "User"."id" = "Posts"."user_id";', + default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[* FROM User; DELETE FROM User;SELECT id] AS [Posts.* FROM User; DELETE FROM User;SELECT id] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];' }); expectsql(sql.selectQuery('User', { attributes: ['name', 'age'], @@ -734,6 +786,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }).include, model: User }, User), { + oracle: 'SELECT "User"."name", "User"."age", "Posts"."id" AS "Posts.id", "Posts"."* FROM User; DELETE FROM User;SELECT id" AS "Posts.data" FROM "User" "User" LEFT OUTER JOIN "Post" "Posts" ON "User"."id" = "Posts"."user_id";', default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[* FROM User; DELETE FROM User;SELECT id] AS [Posts.data] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];' }); @@ -750,6 +803,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }).include, model: User }, User), { + oracle: 'SELECT "User"."name", "User"."age", "Posts"."id" AS "Posts.id", "Posts"."* FROM User; DELETE FROM User;SELECT id" AS "Posts.data" FROM "User" "User" LEFT OUTER JOIN "Post" "Posts" ON "User"."id" = "Posts"."user_id";', default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[* FROM User; DELETE FROM User;SELECT id] AS [Posts.data] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];' }); }); @@ -768,6 +822,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { expectsql(sql.selectQuery('User'), { default: 'SELECT * FROM [User];', postgres: 'SELECT * FROM "User";', + oracle: 'SELECT * FROM "User";', snowflake: 'SELECT * FROM User;' }); }); @@ -778,6 +833,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }), { default: 'SELECT [name], [age] FROM [User];', postgres: 'SELECT name, age FROM "User";', + oracle: 'SELECT name, age FROM "User";', snowflake: 'SELECT name, age FROM User;' }); }); @@ -812,6 +868,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, User), { default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[title] AS [Posts.title] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];', postgres: 'SELECT "User".name, "User".age, Posts.id AS "Posts.id", Posts.title AS "Posts.title" FROM "User" AS "User" LEFT OUTER JOIN Post AS Posts ON "User".id = Posts.user_id;', + oracle: 'SELECT "User".name, "User".age, Posts.id AS "Posts.id", Posts.title AS "Posts.title" FROM "User" "User" LEFT OUTER JOIN Post Posts ON "User".id = Posts.user_id;', snowflake: 'SELECT User.name, User.age, Posts.id AS "Posts.id", Posts.title AS "Posts.title" FROM User AS User LEFT OUTER JOIN Post AS Posts ON User.id = Posts.user_id;' }); }); @@ -859,6 +916,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, User), { default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[title] AS [Posts.title], [Posts->Comments].[id] AS [Posts.Comments.id], [Posts->Comments].[title] AS [Posts.Comments.title], [Posts->Comments].[createdAt] AS [Posts.Comments.createdAt], [Posts->Comments].[updatedAt] AS [Posts.Comments.updatedAt], [Posts->Comments].[post_id] AS [Posts.Comments.post_id] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id] LEFT OUTER JOIN [Comment] AS [Posts->Comments] ON [Posts].[id] = [Posts->Comments].[post_id];', postgres: 'SELECT "User".name, "User".age, Posts.id AS "Posts.id", Posts.title AS "Posts.title", "Posts->Comments".id AS "Posts.Comments.id", "Posts->Comments".title AS "Posts.Comments.title", "Posts->Comments".createdAt AS "Posts.Comments.createdAt", "Posts->Comments".updatedAt AS "Posts.Comments.updatedAt", "Posts->Comments".post_id AS "Posts.Comments.post_id" FROM "User" AS "User" LEFT OUTER JOIN Post AS Posts ON "User".id = Posts.user_id LEFT OUTER JOIN Comment AS "Posts->Comments" ON Posts.id = "Posts->Comments".post_id;', + oracle: 'SELECT "User".name, "User".age, Posts.id AS "Posts.id", Posts.title AS "Posts.title", "Posts->Comments".id AS "Posts.Comments.id", "Posts->Comments".title AS "Posts.Comments.title", "Posts->Comments".createdAt AS "Posts.Comments.createdAt", "Posts->Comments".updatedAt AS "Posts.Comments.updatedAt", "Posts->Comments".post_id AS "Posts.Comments.post_id" FROM "User" "User" LEFT OUTER JOIN Post Posts ON "User".id = Posts.user_id LEFT OUTER JOIN "Comment" "Posts->Comments" ON Posts.id = "Posts->Comments".post_id;', snowflake: 'SELECT User.name, User.age, Posts.id AS "Posts.id", Posts.title AS "Posts.title", "Posts->Comments".id AS "Posts.Comments.id", "Posts->Comments".title AS "Posts.Comments.title", "Posts->Comments".createdAt AS "Posts.Comments.createdAt", "Posts->Comments".updatedAt AS "Posts.Comments.updatedAt", "Posts->Comments".post_id AS "Posts.Comments.post_id" FROM User AS User LEFT OUTER JOIN Post AS Posts ON User.id = Posts.user_id LEFT OUTER JOIN Comment AS "Posts->Comments" ON Posts.id = "Posts->Comments".post_id;' }); }); @@ -896,6 +954,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, User), { default: 'SELECT [User].[name], [User].[age], [User].[status.label], [Posts].[id] AS [Posts.id], [Posts].[title] AS [Posts.title], [Posts].[status.label] AS [Posts.status.label] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];', postgres: 'SELECT "User".name, "User".age, "User"."status.label", Posts.id AS "Posts.id", Posts.title AS "Posts.title", Posts."status.label" AS "Posts.status.label" FROM "User" AS "User" LEFT OUTER JOIN Post AS Posts ON "User".id = Posts.user_id;', + oracle: 'SELECT "User".name, "User".age, "User"."status.label", Posts.id AS "Posts.id", Posts.title AS "Posts.title", Posts."status.label" AS "Posts.status.label" FROM "User" "User" LEFT OUTER JOIN Post Posts ON "User".id = Posts.user_id;', snowflake: 'SELECT User.name, User.age, User."status.label", Posts.id AS "Posts.id", Posts.title AS "Posts.title", Posts."status.label" AS "Posts.status.label" FROM User AS User LEFT OUTER JOIN Post AS Posts ON User.id = Posts.user_id;' }); }); diff --git a/test/unit/sql/show-constraints.test.js b/test/unit/sql/show-constraints.test.js index aff407f49118..56ef9de590df 100644 --- a/test/unit/sql/show-constraints.test.js +++ b/test/unit/sql/show-constraints.test.js @@ -15,6 +15,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: "SELECT CONSTRAINT_CATALOG AS constraintCatalog, CONSTRAINT_NAME AS constraintName, CONSTRAINT_SCHEMA AS constraintSchema, CONSTRAINT_TYPE AS constraintType, TABLE_NAME AS tableName, TABLE_SCHEMA AS tableSchema from INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE table_name='myTable';", db2: 'SELECT CONSTNAME AS "constraintName", TRIM(TABSCHEMA) AS "schemaName", TABNAME AS "tableName" FROM SYSCAT.TABCONST WHERE TABNAME = \'myTable\' ORDER BY CONSTNAME;', snowflake: "SELECT CONSTRAINT_CATALOG AS constraintCatalog, CONSTRAINT_NAME AS constraintName, CONSTRAINT_SCHEMA AS constraintSchema, CONSTRAINT_TYPE AS constraintType, TABLE_NAME AS tableName, TABLE_SCHEMA AS tableSchema from INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE table_name='myTable';", + oracle: "SELECT CONSTRAINT_NAME constraint_name FROM user_cons_columns WHERE table_name = 'myTable'", default: "SELECT sql FROM sqlite_master WHERE tbl_name='myTable';" }); }); @@ -27,6 +28,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: "SELECT CONSTRAINT_CATALOG AS constraintCatalog, CONSTRAINT_NAME AS constraintName, CONSTRAINT_SCHEMA AS constraintSchema, CONSTRAINT_TYPE AS constraintType, TABLE_NAME AS tableName, TABLE_SCHEMA AS tableSchema from INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE table_name='myTable' AND constraint_name = 'myConstraintName';", db2: 'SELECT CONSTNAME AS "constraintName", TRIM(TABSCHEMA) AS "schemaName", TABNAME AS "tableName" FROM SYSCAT.TABCONST WHERE TABNAME = \'myTable\' AND CONSTNAME LIKE \'%myConstraintName%\' ORDER BY CONSTNAME;', snowflake: "SELECT CONSTRAINT_CATALOG AS constraintCatalog, CONSTRAINT_NAME AS constraintName, CONSTRAINT_SCHEMA AS constraintSchema, CONSTRAINT_TYPE AS constraintType, TABLE_NAME AS tableName, TABLE_SCHEMA AS tableSchema from INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE table_name='myTable' AND constraint_name = 'myConstraintName';", + oracle: "SELECT CONSTRAINT_NAME constraint_name FROM user_cons_columns WHERE table_name = 'myTable'", default: "SELECT sql FROM sqlite_master WHERE tbl_name='myTable' AND sql LIKE '%myConstraintName%';" }); }); diff --git a/test/unit/sql/update.test.js b/test/unit/sql/update.test.js index 05205ff20cd3..561206408d90 100644 --- a/test/unit/sql/update.test.js +++ b/test/unit/sql/update.test.js @@ -27,6 +27,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { { query: { db2: 'SELECT * FROM FINAL TABLE (UPDATE "users" SET "user_name"=$1 WHERE "id" = $2);', + oracle: 'UPDATE "users" SET "user_name"=:1 WHERE "id" = :2', default: 'UPDATE [users] SET [user_name]=$1 WHERE [id] = $2' }, bind: { @@ -57,6 +58,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'UPDATE "users" SET "user_name"=$1 WHERE "id" = $2 RETURNING "id","user_name"', db2: 'SELECT * FROM FINAL TABLE (UPDATE "users" SET "user_name"=$1 WHERE "id" = $2);', snowflake: 'UPDATE "users" SET "user_name"=$1 WHERE "id" = $2', + oracle: 'UPDATE "users" SET "user_name"=:1 WHERE "id" = :2', default: 'UPDATE `users` SET `user_name`=$1 WHERE `id` = $2' }, bind: { @@ -85,6 +87,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite: 'UPDATE `Users` SET `username`=$1 WHERE rowid IN (SELECT rowid FROM `Users` WHERE `username` = $2 LIMIT 1)', db2: 'SELECT * FROM FINAL TABLE (UPDATE (SELECT * FROM "Users" WHERE "username" = $2 FETCH NEXT 1 ROWS ONLY) SET "username"=$1);', snowflake: 'UPDATE "Users" SET "username"=$1 WHERE "username" = $2 LIMIT 1', + oracle: 'UPDATE "Users" SET "username"=:1 WHERE "username" = :2 AND rownum <= 1', default: 'UPDATE [Users] SET [username]=$1 WHERE [username] = $2' }, bind: { diff --git a/test/unit/sql/where.test.js b/test/unit/sql/where.test.js index 6ebb967d190d..bd79cc3f7dfc 100644 --- a/test/unit/sql/where.test.js +++ b/test/unit/sql/where.test.js @@ -61,6 +61,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'WHERE "yolo"."User"."id" = 1', snowflake: 'WHERE "yolo"."User"."id" = 1', mariadb: 'WHERE `yolo`.`User`.`id` = 1', + oracle: 'WHERE "yolo"."User"."id" = 1', mssql: 'WHERE [yolo].[User].[id] = 1' }); }); @@ -96,6 +97,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { snowflake: 'WHERE "name" = \'here is a null char: \0\'', mssql: "WHERE [name] = N'here is a null char: \0'", db2: "WHERE \"name\" = 'here is a null char: \0'", + oracle: 'WHERE "name" = \'here is a null char: \0\'', sqlite: "WHERE `name` = 'here is a null char: \0'" }); }); @@ -121,6 +123,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: '"deleted" IS NULL', postgres: '"deleted" IS NULL', snowflake: '"deleted" IS NULL', + oracle: '"deleted" IS NULL', mssql: '[deleted] IS NULL' }); @@ -160,6 +163,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: "`field` = X'53657175656c697a65'", db2: '"field" = BLOB(\'Sequelize\')', snowflake: '"field" = X\'53657175656c697a65\'', + oracle: '"field" = \'53657175656c697a65\'', mssql: '[field] = 0x53657175656c697a65' }); }); @@ -170,6 +174,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { default: '[deleted] IS NOT true', mssql: '[deleted] IS NOT 1', + oracle: '"deleted" IS NOT 1', sqlite: '`deleted` IS NOT 1' }); @@ -495,6 +500,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { [Op.between]: ['2013-01-01', '2013-01-11'] }, { default: "[date] BETWEEN '2013-01-01' AND '2013-01-11'", + oracle: '"date" BETWEEN \'2013-01-01\' AND \'2013-01-11\'', mssql: "[date] BETWEEN N'2013-01-01' AND N'2013-01-11'" }); @@ -505,6 +511,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: "`date` BETWEEN '2013-01-01 00:00:00' AND '2013-01-11 00:00:00'", db2: "\"date\" BETWEEN '2013-01-01 00:00:00' AND '2013-01-11 00:00:00'", snowflake: '"date" BETWEEN \'2013-01-01 00:00:00\' AND \'2013-01-11 00:00:00\'', + oracle: '"date" BETWEEN TO_TIMESTAMP_TZ(\'2013-01-01 00:00:00.000 +00:00\',\'YYYY-MM-DD HH24:MI:SS.FFTZH:TZM\') AND TO_TIMESTAMP_TZ(\'2013-01-11 00:00:00.000 +00:00\',\'YYYY-MM-DD HH24:MI:SS.FFTZH:TZM\')', mariadb: "`date` BETWEEN '2013-01-01 00:00:00.000' AND '2013-01-11 00:00:00.000'" }); @@ -514,13 +521,22 @@ describe(Support.getTestDialectTeaser('SQL'), () => { model: { rawAttributes: { date: { - type: new DataTypes.DATE() + // We need to convert timestamp to a dialect specific date format to work as expected + // For example: Oracle database expects TO_TIMESTAMP_TZ('2013-01-01 00:00:00.000 +00:00','YYYY-MM-DD HH24:MI:SS.FFTZH:TZM') + // from a timestamp instead of '2013-01-01 00:00:00.000 +00:00' which is returned by default DATE class + + // We cannot use - type: new current.dialect.DataTypes.Date - because when dialect is set to 'mysql' + // mysql DATE class returns '2013-01-01 00:00:00' instead of '2013-01-01 00:00:00.000 +00:00'(expected) + // and that would cause this test to fail when dialect is set to 'mysql' + // So we're using - new current.dialect.DataTypes.Date - only in case when dialect is set to 'oracle' as of now + type: current.dialect.name === 'oracle' ? new current.dialect.DataTypes.DATE() : new DataTypes.DATE() } } } }, { default: "[date] BETWEEN '2013-01-01 00:00:00.000 +00:00' AND '2013-01-11 00:00:00.000 +00:00'", + oracle: '"date" BETWEEN TO_TIMESTAMP_TZ(\'2013-01-01 00:00:00.000 +00:00\',\'YYYY-MM-DD HH24:MI:SS.FFTZH:TZM\') AND TO_TIMESTAMP_TZ(\'2013-01-11 00:00:00.000 +00:00\',\'YYYY-MM-DD HH24:MI:SS.FFTZH:TZM\')', mssql: "[date] BETWEEN N'2013-01-01 00:00:00.000 +00:00' AND N'2013-01-11 00:00:00.000 +00:00'" }); @@ -880,6 +896,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: "(\"profile\"#>>'{id}') = CAST('12346-78912' AS TEXT)", sqlite: "json_extract(`profile`,'$.id') = CAST('12346-78912' AS TEXT)", mariadb: "json_unquote(json_extract(`profile`,'$.id')) = CAST('12346-78912' AS CHAR)", + oracle: 'json_value("profile",\'$."id"\') = CAST(\'12346-78912\' AS TEXT)', mysql: "json_unquote(json_extract(`profile`,'$.\\\"id\\\"')) = CAST('12346-78912' AS CHAR)" }); }); @@ -889,6 +906,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: "(\"profile\"#>>'{id}') = '12346-78912' AND (\"profile\"#>>'{name}') = 'test'", sqlite: "json_extract(`profile`,'$.id') = '12346-78912' AND json_extract(`profile`,'$.name') = 'test'", mariadb: "json_unquote(json_extract(`profile`,'$.id')) = '12346-78912' AND json_unquote(json_extract(`profile`,'$.name')) = 'test'", + oracle: 'json_value("profile",\'$."id"\') = \'12346-78912\' AND json_value("profile",\'$."name"\') = \'test\'', mysql: "json_unquote(json_extract(`profile`,'$.\\\"id\\\"')) = '12346-78912' AND json_unquote(json_extract(`profile`,'$.\\\"name\\\"')) = 'test'" }); }); @@ -906,6 +924,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "json_unquote(json_extract(`User`.`data`,'$.nested.attribute')) = 'value'", mysql: "json_unquote(json_extract(`User`.`data`,'$.\\\"nested\\\".\\\"attribute\\\"')) = 'value'", postgres: "(\"User\".\"data\"#>>'{nested,attribute}') = 'value'", + oracle: 'json_value("User"."data",\'$."nested"."attribute"\') = \'value\'', sqlite: "json_extract(`User`.`data`,'$.nested.attribute') = 'value'" }); @@ -921,6 +940,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "CAST(json_unquote(json_extract(`data`,'$.nested')) AS DECIMAL) IN (1, 2)", mysql: "CAST(json_unquote(json_extract(`data`,'$.\\\"nested\\\"')) AS DECIMAL) IN (1, 2)", postgres: "CAST((\"data\"#>>'{nested}') AS DOUBLE PRECISION) IN (1, 2)", + oracle: 'CAST(json_value("data",\'$."nested"\') AS DOUBLE PRECISION) IN (1, 2)', sqlite: "CAST(json_extract(`data`,'$.nested') AS DOUBLE PRECISION) IN (1, 2)" }); @@ -936,6 +956,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "CAST(json_unquote(json_extract(`data`,'$.nested')) AS DECIMAL) BETWEEN 1 AND 2", mysql: "CAST(json_unquote(json_extract(`data`,'$.\\\"nested\\\"')) AS DECIMAL) BETWEEN 1 AND 2", postgres: "CAST((\"data\"#>>'{nested}') AS DOUBLE PRECISION) BETWEEN 1 AND 2", + oracle: 'CAST(json_value("data",\'$."nested"\') AS DOUBLE PRECISION) BETWEEN 1 AND 2', sqlite: "CAST(json_extract(`data`,'$.nested') AS DOUBLE PRECISION) BETWEEN 1 AND 2" }); @@ -955,6 +976,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "(json_unquote(json_extract(`User`.`data`,'$.nested.attribute')) = 'value' AND json_unquote(json_extract(`User`.`data`,'$.nested.prop')) != 'None')", mysql: "(json_unquote(json_extract(`User`.`data`,'$.\\\"nested\\\".\\\"attribute\\\"')) = 'value' AND json_unquote(json_extract(`User`.`data`,'$.\\\"nested\\\".\\\"prop\\\"')) != 'None')", postgres: "((\"User\".\"data\"#>>'{nested,attribute}') = 'value' AND (\"User\".\"data\"#>>'{nested,prop}') != 'None')", + oracle: '(json_value("User"."data",\'$."nested"."attribute"\') = \'value\' AND json_value("User"."data",\'$."nested"."prop"\') != \'None\')', sqlite: "(json_extract(`User`.`data`,'$.nested.attribute') = 'value' AND json_extract(`User`.`data`,'$.nested.prop') != 'None')" }); @@ -974,6 +996,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "(json_unquote(json_extract(`User`.`data`,'$.name.last')) = 'Simpson' AND json_unquote(json_extract(`User`.`data`,'$.employment')) != 'None')", mysql: "(json_unquote(json_extract(`User`.`data`,'$.\\\"name\\\".\\\"last\\\"')) = 'Simpson' AND json_unquote(json_extract(`User`.`data`,'$.\\\"employment\\\"')) != 'None')", postgres: "((\"User\".\"data\"#>>'{name,last}') = 'Simpson' AND (\"User\".\"data\"#>>'{employment}') != 'None')", + oracle: '(json_value("User"."data",\'$."name"."last"\') = \'Simpson\' AND json_value("User"."data",\'$."employment"\') != \'None\')', sqlite: "(json_extract(`User`.`data`,'$.name.last') = 'Simpson' AND json_extract(`User`.`data`,'$.employment') != 'None')" }); @@ -988,6 +1011,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "(CAST(json_unquote(json_extract(`data`,'$.price')) AS DECIMAL) = 5 AND json_unquote(json_extract(`data`,'$.name')) = 'Product')", mysql: "(CAST(json_unquote(json_extract(`data`,'$.\\\"price\\\"')) AS DECIMAL) = 5 AND json_unquote(json_extract(`data`,'$.\\\"name\\\"')) = 'Product')", postgres: "(CAST((\"data\"#>>'{price}') AS DOUBLE PRECISION) = 5 AND (\"data\"#>>'{name}') = 'Product')", + oracle: '(CAST(json_value("data",\'$."price"\') AS DOUBLE PRECISION) = 5 AND json_value("data",\'$."name"\') = \'Product\')', sqlite: "(CAST(json_extract(`data`,'$.price') AS DOUBLE PRECISION) = 5 AND json_extract(`data`,'$.name') = 'Product')" }); @@ -1003,6 +1027,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "json_unquote(json_extract(`data`,'$.nested.attribute')) = 'value'", mysql: "json_unquote(json_extract(`data`,'$.\\\"nested\\\".\\\"attribute\\\"')) = 'value'", postgres: "(\"data\"#>>'{nested,attribute}') = 'value'", + oracle: 'json_value("data",\'$."nested"."attribute"\') = \'value\'', sqlite: "json_extract(`data`,'$.nested.attribute') = 'value'" }); @@ -1018,6 +1043,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "CAST(json_unquote(json_extract(`data`,'$.nested.attribute')) AS DECIMAL) = 4", mysql: "CAST(json_unquote(json_extract(`data`,'$.\\\"nested\\\".\\\"attribute\\\"')) AS DECIMAL) = 4", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) = 4", + oracle: 'CAST(json_value("data",\'$."nested"."attribute"\') AS DOUBLE PRECISION) = 4', sqlite: "CAST(json_extract(`data`,'$.nested.attribute') AS DOUBLE PRECISION) = 4" }); @@ -1035,6 +1061,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "CAST(json_unquote(json_extract(`data`,'$.nested.attribute')) AS DECIMAL) IN (3, 7)", mysql: "CAST(json_unquote(json_extract(`data`,'$.\\\"nested\\\".\\\"attribute\\\"')) AS DECIMAL) IN (3, 7)", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) IN (3, 7)", + oracle: 'CAST(json_value("data",\'$."nested"."attribute"\') AS DOUBLE PRECISION) IN (3, 7)', sqlite: "CAST(json_extract(`data`,'$.nested.attribute') AS DOUBLE PRECISION) IN (3, 7)" }); @@ -1052,6 +1079,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "CAST(json_unquote(json_extract(`data`,'$.nested.attribute')) AS DECIMAL) > 2", mysql: "CAST(json_unquote(json_extract(`data`,'$.\\\"nested\\\".\\\"attribute\\\"')) AS DECIMAL) > 2", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) > 2", + oracle: 'CAST(json_value("data",\'$."nested"."attribute"\') AS DOUBLE PRECISION) > 2', sqlite: "CAST(json_extract(`data`,'$.nested.attribute') AS DOUBLE PRECISION) > 2" }); @@ -1069,6 +1097,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "CAST(json_unquote(json_extract(`data`,'$.nested.attribute')) AS DECIMAL) > 2", mysql: "CAST(json_unquote(json_extract(`data`,'$.\\\"nested\\\".\\\"attribute\\\"')) AS DECIMAL) > 2", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS INTEGER) > 2", + oracle: 'CAST(json_value("data",\'$."nested"."attribute"\') AS INTEGER) > 2', sqlite: "CAST(json_extract(`data`,'$.nested.attribute') AS INTEGER) > 2" }); @@ -1087,6 +1116,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: `CAST(json_unquote(json_extract(\`data\`,'$.nested.attribute')) AS DATETIME) > ${sql.escape(dt)}`, mysql: `CAST(json_unquote(json_extract(\`data\`,'$.\\"nested\\".\\"attribute\\"')) AS DATETIME) > ${sql.escape(dt)}`, postgres: `CAST(("data"#>>'{nested,attribute}') AS TIMESTAMPTZ) > ${sql.escape(dt)}`, + oracle: `json_value("data",'$."nested"."attribute"' RETURNING TIMESTAMP WITH TIME ZONE) > ${sql.escape(dt)}`, sqlite: `json_extract(\`data\`,'$.nested.attribute') > ${sql.escape(dt.toISOString())}` }); @@ -1102,6 +1132,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "json_unquote(json_extract(`data`,'$.nested.attribute')) = 'true'", mysql: "json_unquote(json_extract(`data`,'$.\\\"nested\\\".\\\"attribute\\\"')) = 'true'", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS BOOLEAN) = true", + oracle: 'CAST((CASE WHEN json_value("data",\'$."nested"."attribute"\')=\'true\' THEN 1 ELSE 0 END) AS NUMBER) = 1', sqlite: "CAST(json_extract(`data`,'$.nested.attribute') AS BOOLEAN) = 1" }); @@ -1119,6 +1150,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: "json_unquote(json_extract(`meta_data`,'$.nested.attribute')) = 'value'", mysql: "json_unquote(json_extract(`meta_data`,'$.\\\"nested\\\".\\\"attribute\\\"')) = 'value'", postgres: "(\"meta_data\"#>>'{nested,attribute}') = 'value'", + oracle: 'json_value("meta_data",\'$."nested"."attribute"\') = \'value\'', sqlite: "json_extract(`meta_data`,'$.nested.attribute') = 'value'" }); }); diff --git a/test/unit/transaction.test.js b/test/unit/transaction.test.js index 6fea3c631274..639b9ea9519c 100644 --- a/test/unit/transaction.test.js +++ b/test/unit/transaction.test.js @@ -46,6 +46,9 @@ describe('Transaction', () => { ], mssql: [ 'BEGIN TRANSACTION;' + ], + oracle: [ + 'BEGIN TRANSACTION' ] }; @@ -73,6 +76,10 @@ describe('Transaction', () => { ], mssql: [ 'BEGIN TRANSACTION;' + ], + oracle: [ + 'BEGIN TRANSACTION', + 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED;' ] }; diff --git a/yarn.lock b/yarn.lock index 514efe1607bd..f67e6ab20be9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6393,6 +6393,11 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +oracledb@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/oracledb/-/oracledb-5.4.0.tgz#0ae79faa9a3c04d39b845b897d1e58342d275a85" + integrity sha512-JirwPg+HobTV6EuXcHoHA0n55wJiWCNHaS2TRSHLSxOB9WbDyrT8q6E5PPB7nre1V/kFCd5luHkV1z780Voyfw== + ordered-read-streams@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" From 2cda49bc9e863fdd85bfeb167bd003ca50b8cc8c Mon Sep 17 00:00:00 2001 From: Sudarshan Soma <48428602+sudarshan12s@users.noreply.github.com> Date: Fri, 24 Jun 2022 02:07:53 +0530 Subject: [PATCH 02/14] fix: addressing review comments (#7) * fix: addressing review comments --- .github/workflows/ci.yml | 9 +- .gitignore | 1 + dev/oracle/latest/docker-compose.yml | 2 +- dev/oracle/latest/start.sh | 59 ++- package.json | 9 +- src/dialects/abstract/query-generator.js | 33 +- src/dialects/oracle/connection-manager.js | 33 +- src/dialects/oracle/data-types.js | 19 +- src/dialects/oracle/index.js | 16 +- src/dialects/oracle/query-generator.js | 383 +++++++------------ src/dialects/oracle/query-interface.js | 4 +- src/dialects/oracle/query.js | 207 +++++----- src/sequelize.js | 7 +- src/sql-string.js | 8 +- test/integration/configuration.test.js | 9 +- test/integration/json.test.js | 4 +- test/integration/model.test.js | 12 +- test/integration/model/findAll/order.test.js | 1 + test/integration/query-interface.test.js | 17 +- test/integration/sequelize/query.test.js | 34 +- test/integration/timezone.test.js | 4 +- test/unit/sql/change-column.test.js | 2 +- test/unit/sql/create-table.test.js | 2 +- test/unit/sql/select.test.js | 15 +- 24 files changed, 397 insertions(+), 493 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0011f3ee6b1a..22345c9bc933 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [10, 16] + node-version: [10, 18] name: Oracle DB (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest env: @@ -61,10 +61,11 @@ jobs: SEQ_ORACLE_DB: XEPDB1 SEQ_ORACLE_HOST: localhost SEQ_ORACLE_PORT: 1521 + LD_LIBRARY_PATH: ${{ github.workspace }}/.oracle/instantclient UV_THREADPOOL_SIZE: 128 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: yarn install --frozen-lockfile --ignore-engines @@ -73,7 +74,7 @@ jobs: - name: Unit Tests run: yarn test-unit - name: Integration Tests - run: yarn test-integration-oracle + run: yarn test-integration test-db2: strategy: fail-fast: false diff --git a/.gitignore b/.gitignore index 2544c8581afe..54389f25c6e5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ esdoc node_modules /lib /types +.oracle/ diff --git a/dev/oracle/latest/docker-compose.yml b/dev/oracle/latest/docker-compose.yml index 8c01f794c163..48a217fc78ca 100644 --- a/dev/oracle/latest/docker-compose.yml +++ b/dev/oracle/latest/docker-compose.yml @@ -3,7 +3,7 @@ services: oraclexedb: container_name: oraclexedb - image: gvenzl/oracle-xe:latest + image: gvenzl/oracle-xe:21-slim environment: ORACLE_PASSWORD: password ports: diff --git a/dev/oracle/latest/start.sh b/dev/oracle/latest/start.sh index d407f8567a8f..5988e3be7acc 100755 --- a/dev/oracle/latest/start.sh +++ b/dev/oracle/latest/start.sh @@ -14,31 +14,48 @@ docker-compose -p oraclexedb up -d ./wait-until-healthy.sh oraclexedb # Moving privileges.sql to docker container -docker cp privileges.sql oraclexedb:/opt/oracle/. +docker cp privileges.sql oraclexedb:/opt/oracle/. # Granting all the needed privileges to sequelizetest user docker exec -t oraclexedb sqlplus system/password@XEPDB1 @privileges.sql -# Setting up Oracle instant client for oracledb -if [ ! -d ~/oracle ] && [ $(uname) == 'Linux' ] -then - mkdir ~/oracle && - wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basic-linuxx64.zip --no-check-certificate && - unzip instantclient-basic-linuxx64.zip -d ~/oracle/ && - rm instantclient-basic-linuxx64.zip && - mv ~/oracle/instantclient_21_6 ~/oracle/instantclient - - echo "Local Oracle instant client has been setup!" -elif [ ! -d ~/Downloads/instantclient_19_8 ] && [ $(uname) == 'Darwin' ] +SEQ_WORKSPACE="$PWD"/../../../ + +if [[ ! -d "$SEQ_WORKSPACE"/.oracle/ ]] then - curl -O https://download.oracle.com/otn_software/mac/instantclient/instantclient-basic-macos.dmg && - hdiutil mount instantclient-basic-macos.dmg && - /Volumes/instantclient-basic-macos.x64-19.8.0.0.0dbru/install_ic.sh && - hdiutil unmount /Volumes/instantclient-basic-macos.x64-19.8.0.0.0dbru && - rm instantclient-basic-macos.dmg && - ln -s ~/Downloads/instantclient_19_8/libclntsh.dylib ../../../node_modules/oracledb/build/Release/ - - echo "Local Oracle instant client has been setup!" + mkdir "$SEQ_WORKSPACE"/.oracle/ + if [[ $(uname) == 'Linux' ]] + then + wget https://download.oracle.com/otn_software/linux/instantclient/216000/instantclient-basic-linux.x64-21.6.0.0.0dbru.zip --no-check-certificate && + unzip instantclient-basic-linux.x64-21.6.0.0.0dbru.zip -d "$SEQ_WORKSPACE"/.oracle/ && + rm instantclient-basic-linux.x64-21.6.0.0.0dbru.zip && + mv "$SEQ_WORKSPACE"/.oracle/instantclient_21_6 "$SEQ_WORKSPACE"/.oracle/instantclient + + echo "Local Oracle instant client on Linux has been setup!" + elif [[ $(uname) == 'Darwin' ]] + then + if [[ ! -d ~/Downloads/instantclient_19_8 ]] + then + curl -O https://download.oracle.com/otn_software/mac/instantclient/198000/instantclient-basic-macos.x64-19.8.0.0.0dbru.dmg && + hdiutil mount instantclient-basic-macos.x64-19.8.0.0.0dbru.dmg && + /Volumes/instantclient-basic-macos.x64-19.8.0.0.0dbru/install_ic.sh && + hdiutil unmount /Volumes/instantclient-basic-macos.x64-19.8.0.0.0dbru && + rm instantclient-basic-macos.x64-19.8.0.0.0dbru.dmg && + mv ~/Downloads/instantclient_19_8/ "$SEQ_WORKSPACE"/.oracle/instantclient + else + cp -rf ~/Downloads/instantclient_19_8/ "$SEQ_WORKSPACE"/.oracle/instantclient + fi + ln -s "$SEQ_WORKSPACE"/.oracle/instantclient/libclntsh.dylib "$SEQ_WORKSPACE"/node_modules/oracledb/build/Release/ + + echo "Local Oracle instant client on macOS has been setup!" + else + # Windows + curl -O https://download.oracle.com/otn_software/nt/instantclient/216000/instantclient-basic-windows.x64-21.6.0.0.0dbru.zip && + unzip instantclient-basic-windows.x64-21.6.0.0.0dbru.zip -d "$SEQ_WORKSPACE"/.oracle/ && + rm instantclient-basic-windows.x64-21.6.0.0.0dbru.zip && + mv "$SEQ_WORKSPACE"/.oracle/instantclient_21_6/* "$SEQ_WORKSPACE"/node_modules/oracledb/build/Release + + echo "Local Oracle instant client on $(uname) has been setup!" + fi fi - echo "Local Oracle DB is ready for use!" diff --git a/package.json b/package.json index fa91b479fc80..5a537d4e242a 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,9 @@ }, "tedious": { "optional": true + }, + "oracledb": { + "optional": true } }, "keywords": [ @@ -274,7 +277,7 @@ "test-integration-mssql": "cross-env DIALECT=mssql npm run test-integration", "test-integration-db2": "cross-env DIALECT=db2 npm run test-integration", "test-integration-snowflake": "cross-env DIALECT=snowflake npm run test-integration", - "test-integration-oracle": "cross-env LD_LIBRARY_PATH=$HOME/oracle/instantclient/ DIALECT=oracle UV_THREADPOOL_SIZE=128 npm run test-integration", + "test-integration-oracle": "cross-env LD_LIBRARY_PATH=\"$PWD/.oracle/instantclient/\" DIALECT=oracle UV_THREADPOOL_SIZE=128 npm run test-integration", "test-mariadb": "cross-env DIALECT=mariadb npm test", "test-mysql": "cross-env DIALECT=mysql npm test", "test-sqlite": "cross-env DIALECT=sqlite npm test", @@ -282,7 +285,7 @@ "test-postgres-native": "cross-env DIALECT=postgres-native npm test", "test-mssql": "cross-env DIALECT=mssql npm test", "test-db2": "cross-env DIALECT=db2 npm test", - "test-oracle": "cross-env LD_LIBRARY_PATH=$HOME/oracle/instantclient/ DIALECT=oracle UV_THREADPOOL_SIZE=128 npm test", + "test-oracle": "cross-env LD_LIBRARY_PATH=\"$PWD/.oracle/instantclient/\" DIALECT=oracle UV_THREADPOOL_SIZE=128 npm test", "----------------------------------------- development ---------------------------------------------": "", "sscce": "node sscce.js", "sscce-mariadb": "cross-env DIALECT=mariadb node sscce.js", @@ -292,7 +295,7 @@ "sscce-sqlite": "cross-env DIALECT=sqlite node sscce.js", "sscce-mssql": "cross-env DIALECT=mssql node sscce.js", "sscce-db2": "cross-env DIALECT=db2 node sscce.js", - "sscce-oracle": "cross-env LD_LIBRARY_PATH=$HOME/oracle/instantclient/ DIALECT=oracle node sscce.js", + "sscce-oracle": "cross-env LD_LIBRARY_PATH=\"$PWD/.oracle/instantclient/\" DIALECT=oracle UV_THREADPOOL_SIZE=128 node sscce.js", "prepare": "npm run build && husky install", "build": "node ./build.js", "---------------------------------------------------------------------------------------------------": "" diff --git a/src/dialects/abstract/query-generator.js b/src/dialects/abstract/query-generator.js index eec3cfe368d7..20bdadae41ab 100644 --- a/src/dialects/abstract/query-generator.js +++ b/src/dialects/abstract/query-generator.js @@ -404,15 +404,15 @@ class QueryGenerator { const bindParam = options.bindParam === undefined ? this.bindParam(bind) : options.bindParam; if (this._dialect.supports['LIMIT ON UPDATE'] && options.limit) { - if (this.dialect !== 'mssql' && this.dialect !== 'db2' && this.dialect !== 'oracle') { + if (!['mssql', 'db2', 'oracle'].includes(this.dialect)) { suffix = ` LIMIT ${this.escape(options.limit)} `; } else if (this.dialect === 'oracle') { - //This cannot be setted in where because rownum will be quoted + // This cannot be setted in where because rownum will be quoted if (where && (where.length && where.length > 0 || Object.keys(where).length > 0)) { - //If we have a where clause, we add AND + // If we have a where clause, we add AND suffix += ' AND '; } else { - //No where clause, we add where + // No where clause, we add where suffix += ' WHERE '; } suffix += `rownum <= ${this.escape(options.limit)} `; @@ -994,6 +994,15 @@ class QueryGenerator { return this.quoteIdentifiers(attribute); } + /** + * Returns the alias token + * + * @returns {string} + */ + getAliasToken() { + return 'AS'; + } + /** * Quote table name with optional alias and schema attribution * @@ -1029,7 +1038,7 @@ class QueryGenerator { } if (alias) { - table += ` AS ${this.quoteIdentifier(alias)}`; + table += ` ${this.getAliasToken()} ${this.quoteIdentifier(alias)}`; } return table; @@ -1219,7 +1228,6 @@ class QueryGenerator { let mainJoinQueries = []; let subJoinQueries = []; let query; - const hasAs = this._dialect.name === 'oracle' ? '' : 'AS '; // Aliases can be passed through subqueries and we don't want to reset them if (this.options.minifyAliases && !options.aliasesMapping) { @@ -1369,7 +1377,7 @@ class QueryGenerator { model }, model - ).replace(/;$/, '')}) ${hasAs}sub`; // Every derived table must have its own alias + ).replace(/;$/, '')}) ${this.getAliasToken()} sub`; // Every derived table must have its own alias const placeHolder = this.whereItemQuery(Op.placeholder, true, { model }); const splicePos = baseQuery.indexOf(placeHolder); @@ -1463,7 +1471,7 @@ class QueryGenerator { if (subQuery) { this._throwOnEmptyAttributes(attributes.main, { modelName: model && model.name, as: mainTable.as }); - query = `SELECT ${attributes.main.join(', ')} FROM (${subQueryItems.join('')}) ${hasAs}${mainTable.as}${mainJoinQueries.join('')}${mainQueryItems.join('')}`; + query = `SELECT ${attributes.main.join(', ')} FROM (${subQueryItems.join('')}) ${this.getAliasToken()} ${mainTable.as}${mainJoinQueries.join('')}${mainQueryItems.join('')}`; } else { query = mainQueryItems.join(''); } @@ -2167,7 +2175,7 @@ class QueryGenerator { let fragment = `SELECT ${attributes.join(', ')} FROM ${tables}`; if (mainTableAs) { - fragment += ` AS ${mainTableAs}`; + fragment += ` ${this.getAliasToken()} ${mainTableAs}`; } if (options.indexHints && this._dialect.supports.indexHints) { @@ -2821,6 +2829,13 @@ class QueryGenerator { booleanValue(value) { return value; } + + /** + * Returns the authenticate test query string + */ + authTestQuery() { + return 'SELECT 1+1 AS result'; + } } Object.assign(QueryGenerator.prototype, require('./query-generator/operators')); diff --git a/src/dialects/oracle/connection-manager.js b/src/dialects/oracle/connection-manager.js index c8359669baac..bd2c62e1b82c 100644 --- a/src/dialects/oracle/connection-manager.js +++ b/src/dialects/oracle/connection-manager.js @@ -18,7 +18,7 @@ const { promisify } = require('util'); * * @private */ -class ConnectionManager extends AbstractConnectionManager { +export class OracleConnectionManager extends AbstractConnectionManager { constructor(dialect, sequelize) { super(dialect, sequelize); @@ -118,13 +118,6 @@ class ConnectionManager extends AbstractConnectionManager { // We check if there are dialect options if (config.dialectOptions) { - // const dialectOptions = config.dialectOptions; - - // //If stmtCacheSize is defined, we set it - // if (dialectOptions && 'stmtCacheSize' in dialectOptions) { - // connectionConfig.stmtCacheSize = dialectOptions.stmtCacheSize; - // } - Object.keys(config.dialectOptions).forEach(key => { connectionConfig[key] = config.dialectOptions[key]; }); @@ -150,17 +143,19 @@ class ConnectionManager extends AbstractConnectionManager { errorCode = errorCode[0]; switch (errorCode) { - case 'ORA-28000': // Account locked - case 'ORA-12541': // ORA-12541: TNS:No listener + case 'ORA-12560': // ORA-12560: TNS: Protocol Adapter Error + case 'ORA-12154': // ORA-12154: TNS: Could not resolve the connect identifier specified + case 'ORA-12505': // ORA-12505: TNS: Listener does not currently know of SID given in connect descriptor + case 'ORA-12514': // ORA-12514: TNS: Listener does not currently know of service requested in connect descriptor throw new SequelizeErrors.ConnectionRefusedError(err); - case 'ORA-01017': // ORA-01017 : invalid username/password; logon denied + case 'ORA-28000': // ORA-28000: Account locked + case 'ORA-28040': // ORA-28040: No matching authentication protocol + case 'ORA-01017': // ORA-01017: invalid username/password; logon denied throw new SequelizeErrors.AccessDeniedError(err); - case 'ORA-12154': - throw new SequelizeErrors.HostNotReachableError(err); //ORA-12154: TNS:could not resolve the connect identifier specified - case 'ORA-12514': // ORA-12514: TNS:listener does not currently know of service requested in connect descriptor - throw new SequelizeErrors.HostNotFoundError(err); - // case 'ORA-12541': // ORA-12541: TNS:No listener - // throw new SequelizeErrors.AccessDeniedError(err); + case 'ORA-12541': // ORA-12541: TNS: No listener + throw new SequelizeErrors.HostNotReachableError(err); + case 'ORA-12170': // ORA-12170: TNS: Connect Timeout occurred + throw new SequelizeErrors.ConnectionTimedOutError(err); default: throw new SequelizeErrors.ConnectionError(err); } @@ -186,7 +181,3 @@ class ConnectionManager extends AbstractConnectionManager { return connection && connection.isHealthy(); } } - -module.exports = ConnectionManager; -module.exports.ConnectionManager = ConnectionManager; -module.exports.default = ConnectionManager; diff --git a/src/dialects/oracle/data-types.js b/src/dialects/oracle/data-types.js index d00024881b78..c8807a2ff910 100644 --- a/src/dialects/oracle/data-types.js +++ b/src/dialects/oracle/data-types.js @@ -8,7 +8,7 @@ const momentTz = require('moment-timezone'); module.exports = BaseTypes => { const warn = BaseTypes.ABSTRACT.warn.bind( undefined, - 'https://docs.oracle.com/database/122/SQLRF/Data-Types.htm#SQLRF30020' + 'https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=GUID-D424D23B-0933-425F-BC69-9C0E6724693C' ); BaseTypes.DATE.types.oracle = ['TIMESTAMP', 'TIMESTAMP WITH LOCAL TIME ZONE']; @@ -36,7 +36,7 @@ module.exports = BaseTypes => { toSql() { if (this.length > 4000 || this._binary && this._length > 2000) { warn( - 'Oracle 12 supports length up to 32764; be sure that your administrator has extended the MAX_STRING_SIZE parameter. Check https://docs.oracle.com/database/121/REFRN/GUID-D424D23B-0933-425F-BC69-9C0E6724693C.htm#REFRN10321' + 'Oracle supports length up to 32764 bytes or characters; Be sure that your administrator has extended the MAX_STRING_SIZE parameter. Check https://docs.oracle.com/pls/topic/lookup?ctx=dblatest&id=GUID-7B72E154-677A-4342-A1EA-C74C1EA928E6' ); } if (!this._binary) { @@ -145,6 +145,7 @@ module.exports = BaseTypes => { class CHAR extends BaseTypes.CHAR { toSql() { if (this._binary) { + warn('CHAR.BINARY datatype is not of Fixed Length.'); return `RAW(${this._length})`; } return super.toSql(); @@ -286,6 +287,7 @@ module.exports = BaseTypes => { // ORACLE does not support any options for bigint if (this._length || this.options.length || this._unsigned || this._zerofill) { + warn('Oracle does not support BIGINT with options'); this._length = undefined; this.options.length = undefined; this._unsigned = undefined; @@ -329,12 +331,12 @@ module.exports = BaseTypes => { return 'BINARY_DOUBLE'; } - // https://docs.oracle.com/cd/E37502_01/server.751/es_eql/src/ceql_literals_nan.html + // https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=GUID-0BA2E065-8006-426C-A3CB-1F6B0C8F283C _stringify(value) { - if (value === 'Infinity') { + if (value === Number.POSITIVE_INFINITY) { return 'inf'; } - if (value === '-Infinity') { + if (value === Number.NEGATIVE_INFINITY) { return '-inf'; } return value; @@ -385,6 +387,7 @@ module.exports = BaseTypes => { BaseTypes.DOUBLE.apply(this, arguments); if (this._length || this._unsigned || this._zerofill) { + warn('Oracle does not support DOUBLE with options.'); this._length = undefined; this.options.length = undefined; this._unsigned = undefined; @@ -414,13 +417,13 @@ module.exports = BaseTypes => { return value; } - _stringify(date) { + _stringify(date, options) { // If date is not null only then we format the date if (date) { const format = 'YYYY/MM/DD'; - return `TO_DATE('${date}','${format}')`; + return options.escape(`TO_DATE('${date}','${format}')`); } - return date; + return options.escape(date); } _getBindDef(oracledb) { diff --git a/src/dialects/oracle/index.js b/src/dialects/oracle/index.js index 8222eaf63cbc..2514d767fe89 100644 --- a/src/dialects/oracle/index.js +++ b/src/dialects/oracle/index.js @@ -3,10 +3,10 @@ 'use strict'; const _ = require('lodash'); -const AbstractDialect = require('../abstract'); -const ConnectionManager = require('./connection-manager'); -const Query = require('./query'); -const QueryGenerator = require('./query-generator'); +const { AbstractDialect } = require('../abstract'); +const { OracleConnectionManager } = require('./connection-manager'); +const { OracleQuery } = require('./query'); +const { OracleQueryGenerator } = require('./query-generator'); const DataTypes = require('../../data-types').oracle; const { OracleQueryInterface } = require('./query-interface'); @@ -14,9 +14,9 @@ class OracleDialect extends AbstractDialect { constructor(sequelize) { super(); this.sequelize = sequelize; - this.connectionManager = new ConnectionManager(this, sequelize); + this.connectionManager = new OracleConnectionManager(this, sequelize); this.connectionManager.initPools(); - this.queryGenerator = new QueryGenerator({ + this.queryGenerator = new OracleQueryGenerator({ _dialect: this, sequelize }); @@ -55,8 +55,8 @@ OracleDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype }); OracleDialect.prototype.defaultVersion = '18.4.0'; -OracleDialect.prototype.Query = Query; -OracleDialect.prototype.queryGenerator = QueryGenerator; +OracleDialect.prototype.Query = OracleQuery; +OracleDialect.prototype.queryGenerator = OracleQueryGenerator; OracleDialect.prototype.DataTypes = DataTypes; OracleDialect.prototype.name = 'oracle'; OracleDialect.prototype.TICK_CHAR = '"'; diff --git a/src/dialects/oracle/query-generator.js b/src/dialects/oracle/query-generator.js index dc4583e370d6..cfdee9634be9 100644 --- a/src/dialects/oracle/query-generator.js +++ b/src/dialects/oracle/query-generator.js @@ -2,7 +2,6 @@ 'use strict'; -/* jshint -W110 */ const Utils = require('../../utils'); const DataTypes = require('../../data-types'); const AbstractQueryGenerator = require('../abstract/query-generator'); @@ -12,7 +11,7 @@ const Transaction = require('../../transaction'); /** * list of reserved words in Oracle DB 21c - * source: https://docs.oracle.com/en/cloud/saas/taleo-enterprise/21d/otccu/r-reservedwords.html#id08ATA0RF05Z + * source: https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=GUID-7B72E154-677A-4342-A1EA-C74C1EA928E6 * * @private */ @@ -21,7 +20,7 @@ const JSON_FUNCTION_REGEX = /^\s*((?:[a-z]+_){0,2}jsonb?(?:_[a-z]+){0,2})\([^)]* const JSON_OPERATOR_REGEX = /^\s*(->>?|@>|<@|\?[|&]?|\|{2}|#-)/i; const TOKEN_CAPTURE_REGEX = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i; -class OracleQueryGenerator extends AbstractQueryGenerator { +export class OracleQueryGenerator extends AbstractQueryGenerator { constructor(options) { super(options); } @@ -55,46 +54,48 @@ class OracleQueryGenerator extends AbstractQueryGenerator { } createSchema(schema) { - const quotedSchema = this.quoteTable(schema); - const schemaName = this.getCatalogName(schema); + const quotedSchema = this.quoteIdentifier(schema); return [ 'DECLARE', - ' V_COUNT INTEGER;', - ' V_CURSOR_NAME INTEGER;', - ' V_RET INTEGER;', + 'USER_FOUND BOOLEAN := FALSE;', 'BEGIN', - ' SELECT COUNT(1) INTO V_COUNT FROM ALL_USERS WHERE USERNAME = ', - wrapSingleQuote(schemaName), + ' BEGIN', + ' EXECUTE IMMEDIATE ', + this.escape(`CREATE USER ${quotedSchema} IDENTIFIED BY 12345 DEFAULT TABLESPACE USERS`), ';', - ' IF V_COUNT = 0 THEN', + ' EXCEPTION WHEN OTHERS THEN', + ' IF SQLCODE != -1920 THEN', + ' RAISE;', + ' ELSE', + ' USER_FOUND := TRUE;', + ' END IF;', + ' END;', + ' IF NOT USER_FOUND THEN', ' EXECUTE IMMEDIATE ', - wrapSingleQuote(`CREATE USER ${ quotedSchema } IDENTIFIED BY 12345 DEFAULT TABLESPACE USERS`), + this.escape(`GRANT "CONNECT" TO ${quotedSchema}`), ';', ' EXECUTE IMMEDIATE ', - wrapSingleQuote(`GRANT "CONNECT" TO ${quotedSchema}`), + this.escape(`GRANT CREATE TABLE TO ${quotedSchema}`), ';', ' EXECUTE IMMEDIATE ', - wrapSingleQuote(`GRANT create table TO ${quotedSchema}`), + this.escape(`GRANT CREATE VIEW TO ${quotedSchema}`), ';', ' EXECUTE IMMEDIATE ', - wrapSingleQuote(`GRANT create view TO ${quotedSchema}`), + this.escape(`GRANT CREATE ANY TRIGGER TO ${quotedSchema}`), ';', ' EXECUTE IMMEDIATE ', - wrapSingleQuote(`GRANT create any trigger TO ${quotedSchema}`), + this.escape(`GRANT CREATE ANY PROCEDURE TO ${quotedSchema}`), ';', ' EXECUTE IMMEDIATE ', - wrapSingleQuote(`GRANT create any procedure TO ${quotedSchema}`), + this.escape(`GRANT CREATE SEQUENCE TO ${quotedSchema}`), ';', ' EXECUTE IMMEDIATE ', - wrapSingleQuote(`GRANT create sequence TO ${quotedSchema}`), + this.escape(`GRANT CREATE SYNONYM TO ${quotedSchema}`), ';', ' EXECUTE IMMEDIATE ', - wrapSingleQuote(`GRANT create synonym TO ${quotedSchema}`), + this.escape(`ALTER USER ${quotedSchema} QUOTA UNLIMITED ON USERS`), ';', - ' EXECUTE IMMEDIATE ', - wrapSingleQuote(`ALTER USER ${quotedSchema} QUOTA UNLIMITED ON USERS`), - ';', - ' END IF;', + ' END IF;', 'END;' ].join(' '); } @@ -104,19 +105,14 @@ class OracleQueryGenerator extends AbstractQueryGenerator { } dropSchema(schema) { - const schemaName = this.getCatalogName(schema); return [ - 'DECLARE', - ' V_COUNT INTEGER;', 'BEGIN', - ' V_COUNT := 0;', - ' SELECT COUNT(1) INTO V_COUNT FROM ALL_USERS WHERE USERNAME = ', - wrapSingleQuote(schemaName), - ';', - ' IF V_COUNT != 0 THEN', - ' EXECUTE IMMEDIATE ', - wrapSingleQuote(`DROP USER ${this.quoteTable(schema)} CASCADE`), + 'EXECUTE IMMEDIATE ', + this.escape(`DROP USER ${this.quoteTable(schema)} CASCADE`), ';', + 'EXCEPTION WHEN OTHERS THEN', + ' IF SQLCODE != -1918 THEN', + ' RAISE;', ' END IF;', 'END;' ].join(' '); @@ -128,70 +124,48 @@ class OracleQueryGenerator extends AbstractQueryGenerator { createTableQuery(tableName, attributes, options) { const primaryKeys = [], - foreignKeys = {}, + foreignKeys = Object.create(null), attrStr = [], - self = this, checkStr = []; const values = { table: this.quoteTable(tableName) }; - const chkRegex = /CHECK \(([a-zA-Z_.0-9]*) (.*)\)/g; // Check regex - // Starting by dealing with all attributes for (let attr in attributes) { - if (Object.prototype.hasOwnProperty.call(attributes, attr)) { - const dataType = attributes[attr]; - let match; - - attr = this.quoteIdentifier(attr); - - // ORACLE doesn't support inline REFERENCES declarations: move to the end - if (_.includes(dataType, 'PRIMARY KEY')) { - // Primary key - primaryKeys.push(attr); - if (_.includes(dataType, 'REFERENCES')) { - match = dataType.match(/^(.+) (REFERENCES.*)$/); - attrStr.push(`${attr} ${match[1].replace(/PRIMARY KEY/, '')}`); - - // match[2] already has foreignKeys in correct format so we don't need to replace - foreignKeys[attr] = match[2]; - } else { - attrStr.push(`${attr} ${dataType.replace(/PRIMARY KEY/, '').trim()}`); - } - } else if (_.includes(dataType, 'REFERENCES')) { - // Foreign key - match = dataType.match(/^(.+) (REFERENCES.*)$/); - attrStr.push(`${attr} ${match[1]}`); + if (!Object.prototype.hasOwnProperty.call(attributes, attr)) continue; + const dataType = attributes[attr]; + attr = this.quoteIdentifier(attr); + + // ORACLE doesn't support inline REFERENCES declarations: move to the end + if (dataType.includes('PRIMARY KEY')) { + // Primary key + primaryKeys.push(attr); + if (dataType.includes('REFERENCES')) { + const match = dataType.match(/^(.+) (REFERENCES.*)$/); + attrStr.push(`${attr} ${match[1].replace(/PRIMARY KEY/, '')}`); // match[2] already has foreignKeys in correct format so we don't need to replace foreignKeys[attr] = match[2]; - } else if (_.includes(dataType, 'CHECK')) { - // Check constraints go to the end - match = dataType.match(/^(.+) (CHECK.*)$/); - attrStr.push(`${attr} ${match[1]}`); - match[2] = match[2].replace('ATTRIBUTENAME', attr); - const checkCond = match[2].replace(chkRegex, (match, column, condition) => { - return `CHECK (${this.quoteIdentifier(column)} ${condition})`; - }); - - checkStr.push(checkCond); } else { - attrStr.push(`${attr} ${dataType}`); + attrStr.push(`${attr} ${dataType.replace(/PRIMARY KEY/, '').trim()}`); } + } else if (dataType.includes('REFERENCES')) { + // Foreign key + const match = dataType.match(/^(.+) (REFERENCES.*)$/); + attrStr.push(`${attr} ${match[1]}`); + + // match[2] already has foreignKeys in correct format so we don't need to replace + foreignKeys[attr] = match[2]; + } else { + attrStr.push(`${attr} ${dataType}`); } } values['attributes'] = attrStr.join(', '); - const pkString = primaryKeys - .map( - (pk => { - return this.quoteIdentifier(pk); - }).bind(this) - ) - .join(', '); + const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', '); if (pkString.length > 0) { // PrimarykeyName would be of form "PK_table_col" @@ -219,13 +193,12 @@ class OracleQueryGenerator extends AbstractQueryGenerator { // Dealing with FKs for (const fkey in foreignKeys) { - if (Object.prototype.hasOwnProperty.call(foreignKeys, fkey)) { - // Oracle default response for FK, doesn't support if defined - if (foreignKeys[fkey].indexOf('ON DELETE NO ACTION') > -1) { - foreignKeys[fkey] = foreignKeys[fkey].replace('ON DELETE NO ACTION', ''); - } - values.attributes += `,FOREIGN KEY (${this.quoteIdentifier(fkey)}) ${foreignKeys[fkey]}`; + if (!Object.prototype.hasOwnProperty.call(foreignKeys, fkey)) continue; + // Oracle default response for FK, doesn't support if defined + if (foreignKeys[fkey].indexOf('ON DELETE NO ACTION') > -1) { + foreignKeys[fkey] = foreignKeys[fkey].replace('ON DELETE NO ACTION', ''); } + values.attributes += `,FOREIGN KEY (${this.quoteIdentifier(fkey)}) ${foreignKeys[fkey]}`; } if (checkStr.length > 0) { @@ -352,10 +325,10 @@ class OracleQueryGenerator extends AbstractQueryGenerator { // Add unique column again when it doesn't find unique constraint name after doing showIndexQuery // MYSQL doesn't support constraint name > 64 and they face similar issue if size exceed 64 chars if (indexName.length > 128) { - values.attributes += `,UNIQUE (${columns.fields.map(field => self.quoteIdentifier(field)).join(', ') })`; + values.attributes += `,UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ') })`; } else { values.attributes += - `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => self.quoteIdentifier(field)).join(', ') })`; + `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ') })`; } } }); @@ -366,12 +339,12 @@ class OracleQueryGenerator extends AbstractQueryGenerator { 'CREATE TABLE', values.table, `(${values.attributes})` - ]).replace(/'/g, "''"); + ]); return Utils.joinSQLFragments([ 'BEGIN', 'EXECUTE IMMEDIATE', - `'${query}';`, + `${this.escape(query)};`, 'EXCEPTION WHEN OTHERS THEN', 'IF SQLCODE != -955 THEN', 'RAISE;', @@ -382,7 +355,7 @@ class OracleQueryGenerator extends AbstractQueryGenerator { tableExistsQuery(table) { const [tableName, schemaName] = this.getSchemaNameAndTableName(table); - return `SELECT TABLE_NAME FROM ALL_TABLES WHERE TABLE_NAME = ${wrapSingleQuote(tableName)} AND OWNER = ${table.schema ? wrapSingleQuote(schemaName) : 'USER'}`; + return `SELECT TABLE_NAME FROM ALL_TABLES WHERE TABLE_NAME = ${this.escape(tableName)} AND OWNER = ${table.schema ? this.escape(schemaName) : 'USER'}`; } /** @@ -407,9 +380,9 @@ class OracleQueryGenerator extends AbstractQueryGenerator { 'FROM all_tab_columns atc ', 'LEFT OUTER JOIN all_cons_columns ucc ON(atc.table_name = ucc.table_name AND atc.COLUMN_NAME = ucc.COLUMN_NAME ) ', schema - ? `WHERE (atc.OWNER = ${wrapSingleQuote(schema)}) ` + ? `WHERE (atc.OWNER = ${this.escape(schema)}) ` : 'WHERE atc.OWNER = USER ', - `AND (atc.TABLE_NAME = ${wrapSingleQuote(currTableName)})`, + `AND (atc.TABLE_NAME = ${this.escape(currTableName)})`, 'ORDER BY "PRIMARY", atc.COLUMN_NAME' ].join(''); } @@ -425,7 +398,7 @@ class OracleQueryGenerator extends AbstractQueryGenerator { showConstraintsQuery(table) { const tableName = this.getCatalogName(table.tableName || table); - return `SELECT CONSTRAINT_NAME constraint_name FROM user_cons_columns WHERE table_name = ${wrapSingleQuote(tableName)}`; + return `SELECT CONSTRAINT_NAME constraint_name FROM user_cons_columns WHERE table_name = ${this.escape(tableName)}`; } showTablesQuery() { @@ -463,12 +436,7 @@ class OracleQueryGenerator extends AbstractQueryGenerator { const constraintSnippet = this.getConstraintSnippet(tableName, options); - if (typeof tableName === 'string') { - tableName = this.quoteIdentifier(tableName); - } else { - tableName = this.quoteTable(tableName); - } - + tableName = this.quoteTable(tableName); return `ALTER TABLE ${tableName} ADD ${constraintSnippet};`; } @@ -478,10 +446,9 @@ class OracleQueryGenerator extends AbstractQueryGenerator { const attribute = Utils.joinSQLFragments([ this.quoteIdentifier(key), this.attributeToSQL(dataType, { + attributeName: key, context: 'addColumn' }) - .replace('ATTRIBUTENAME', this.quoteIdentifier(key)) - .replace(/'/g, "'") ]); return Utils.joinSQLFragments([ @@ -512,34 +479,31 @@ class OracleQueryGenerator extends AbstractQueryGenerator { * Since sequelize doesn't support multiple column foreign key, added complexity to * add the feature isn't needed * - * @param {Array} sql Array with sql blocks * @param {string} definition The operation that needs to be performed on the attribute * @param {string|object} table The table that needs to be altered * @param {string} attributeName The name of the attribute which would get altered */ - _alterForeignKeyConstraint(sql, definition, table, attributeName) { + _alterForeignKeyConstraint(definition, table, attributeName) { const [tableName, schemaName] = this.getSchemaNameAndTableName(table); - const attributeNameConstant = wrapSingleQuote(this.getCatalogName(attributeName)); - const schemaNameConstant = table.schema ? wrapSingleQuote(this.getCatalogName(schemaName)) : 'USER'; - const tableNameConstant = wrapSingleQuote(this.getCatalogName(tableName)); + const attributeNameConstant = this.escape(this.getCatalogName(attributeName)); + const schemaNameConstant = table.schema ? this.escape(this.getCatalogName(schemaName)) : 'USER'; + const tableNameConstant = this.escape(this.getCatalogName(tableName)); const getConsNameQuery = [ - 'select constraint_name into cons_name', - 'from (', - ' select distinct cc.owner, cc.table_name, cc.constraint_name,', - ' cc.column_name', - ' as cons_columns', - ' from all_cons_columns cc, all_constraints c', - ' where cc.owner = c.owner', - ' and cc.table_name = c.table_name', - ' and cc.constraint_name = c.constraint_name', - ' and c.constraint_type = \'R\'', - ' group by cc.owner, cc.table_name, cc.constraint_name, cc.column_name', + 'SELECT constraint_name INTO cons_name', + 'FROM (', + ' SELECT DISTINCT cc.owner, cc.table_name, cc.constraint_name, cc.column_name AS cons_columns', + ' FROM all_cons_columns cc, all_constraints c', + ' WHERE cc.owner = c.owner', + ' AND cc.table_name = c.table_name', + ' AND cc.constraint_name = c.constraint_name', + ' AND c.constraint_type = \'R\'', + ' GROUP BY cc.owner, cc.table_name, cc.constraint_name, cc.column_name', ')', - 'where owner =', + 'WHERE owner =', schemaNameConstant, - 'and table_name =', + 'AND table_name =', tableNameConstant, - 'and cons_columns =', + 'AND cons_columns =', attributeNameConstant, ';' ].join(' '); @@ -549,7 +513,7 @@ class OracleQueryGenerator extends AbstractQueryGenerator { `(${this.quoteIdentifier(attributeName)})`, definition.replace(/.+?(?=REFERENCES)/, '') ]); - const fullQuery = [ + return [ 'BEGIN', getConsNameQuery, 'EXCEPTION', @@ -559,21 +523,19 @@ class OracleQueryGenerator extends AbstractQueryGenerator { 'IF CONS_NAME IS NOT NULL THEN', ` EXECUTE IMMEDIATE 'ALTER TABLE ${this.quoteTable(table)} DROP CONSTRAINT "'||CONS_NAME||'"';`, 'END IF;', - `EXECUTE IMMEDIATE '${secondQuery}';` + `EXECUTE IMMEDIATE ${this.escape(secondQuery)};` ].join(' '); - sql.push(fullQuery); } /** * Function to alter table modify * - * @param {Array} sql Array with sql blocks * @param {string} definition The operation that needs to be performed on the attribute * @param {object|string} table The table that needs to be altered * @param {string} attributeName The name of the attribute which would get altered */ - _modifyQuery(sql, definition, table, attributeName) { - let query = Utils.joinSQLFragments([ + _modifyQuery(definition, table, attributeName) { + const query = Utils.joinSQLFragments([ 'ALTER TABLE', this.quoteTable(table), 'MODIFY', @@ -581,20 +543,19 @@ class OracleQueryGenerator extends AbstractQueryGenerator { definition ]); const secondQuery = query.replace('NOT NULL', '').replace('NULL', ''); - query = [ + return [ 'BEGIN', - `EXECUTE IMMEDIATE '${query}';`, + `EXECUTE IMMEDIATE ${this.escape(query)};`, 'EXCEPTION', 'WHEN OTHERS THEN', ' IF SQLCODE = -1442 OR SQLCODE = -1451 THEN', // We execute the statement without the NULL / NOT NULL clause if the first statement failed due to this - ` EXECUTE IMMEDIATE '${secondQuery}';`, + ` EXECUTE IMMEDIATE ${this.escape(secondQuery)};`, ' ELSE', ' RAISE;', ' END IF;', 'END;' ].join(' '); - sql.push(query); } changeColumnQuery(table, attributes) { @@ -604,15 +565,13 @@ class OracleQueryGenerator extends AbstractQueryGenerator { 'BEGIN' ]; for (const attributeName in attributes) { - let definition = attributes[attributeName]; + if (!Object.prototype.hasOwnProperty.call(attributes, attributeName)) continue; + const definition = attributes[attributeName]; if (definition.match(/REFERENCES/)) { - this._alterForeignKeyConstraint(sql, definition, table, attributeName); + sql.push(this._alterForeignKeyConstraint(definition, table, attributeName)); } else { - if (definition.indexOf('CHECK') > -1) { - definition = definition.replace(/'/g, "''"); - } // Building the modify query - this._modifyQuery(sql, definition, table, attributeName); + sql.push(this._modifyQuery(definition, table, attributeName)); } } sql.push('END;'); @@ -638,13 +597,13 @@ class OracleQueryGenerator extends AbstractQueryGenerator { */ getInsertQueryReturnIntoBinds(returnAttributes, inbindLength, returningModelAttributes, returnTypes, options) { const oracledb = this.sequelize.connectionManager.lib; - const outBindAttributes = {}; + const outBindAttributes = Object.create(null); const outbind = []; const outbindParam = this.bindParam(outbind, inbindLength); returningModelAttributes.forEach((element, index) => { // generateReturnValues function quotes identifier based on the quoteIdentifier option // If the identifier starts with a quote we remove it else we use it as is - if (_.startsWith(element, '"')) { + if (element.startsWith('"')) { element = element.substring(1, element.length - 1); } outBindAttributes[element] = Object.assign(returnTypes[index]._getBindDef(oracledb), { dir: oracledb.BIND_OUT }); @@ -873,7 +832,7 @@ class OracleQueryGenerator extends AbstractQueryGenerator { if (options.limit) { const whereTmpl = where ? ` AND ${where}` : ''; queryTmpl = - `DELETE FROM ${this.quoteTable(table)} WHERE rowid IN (SELECT rowid FROM ${this.quoteTable(table)} WHERE rownum <= ${options.limit}${ + `DELETE FROM ${this.quoteTable(table)} WHERE rowid IN (SELECT rowid FROM ${this.quoteTable(table)} WHERE rownum <= ${this.escape(options.limit)}${ whereTmpl })`; } else { @@ -890,10 +849,10 @@ class OracleQueryGenerator extends AbstractQueryGenerator { 'FROM all_ind_columns i ', 'INNER JOIN all_indexes u ', 'ON (u.table_name = i.table_name AND u.index_name = i.index_name) ', - `WHERE i.table_name = ${wrapSingleQuote(tableName)}`, - ' AND u.TABLE_OWNER = ', - owner ? wrapSingleQuote(owner) : 'USER', - ' ORDER BY INDEX_NAME, COLUMN_POSITION' + `WHERE i.table_name = ${this.escape(tableName)}`, + ' AND u.table_owner = ', + owner ? this.escape(owner) : 'USER', + ' ORDER BY index_name, column_position' ]; return sql.join(''); @@ -940,7 +899,7 @@ class OracleQueryGenerator extends AbstractQueryGenerator { // enums are a special case template = attribute.type.toSql(); template += - ` CHECK (ATTRIBUTENAME IN(${ + ` CHECK (${this.quoteIdentifier(options.attributeName)} IN(${ _.map(attribute.values, value => { return this.escape(value); }).join(', ') @@ -949,13 +908,13 @@ class OracleQueryGenerator extends AbstractQueryGenerator { } if (attribute.type instanceof DataTypes.JSON) { template = attribute.type.toSql(); - template += ' CHECK (ATTRIBUTENAME IS JSON)'; + template += ` CHECK (${this.quoteIdentifier(options.attributeName)} IS JSON)`; return template; } if (attribute.type instanceof DataTypes.BOOLEAN) { template = attribute.type.toSql(); template += - ' CHECK (ATTRIBUTENAME IN(\'1\', \'0\'))'; + ` CHECK (${this.quoteIdentifier(options.attributeName)} IN('1', '0'))`; return template; } if (attribute.autoIncrement) { @@ -1025,7 +984,7 @@ class OracleQueryGenerator extends AbstractQueryGenerator { for (const key in attributes) { const attribute = attributes[key]; const attributeName = attribute.field || key; - result[attributeName] = this.attributeToSQL(attribute, options).replace('ATTRIBUTENAME', this.quoteIdentifier(attributeName)); + result[attributeName] = this.attributeToSQL(attribute, { attributeName, ...options }); } return result; @@ -1060,11 +1019,11 @@ class OracleQueryGenerator extends AbstractQueryGenerator { column = this.getCatalogName(column); const sql = [ 'SELECT CONSTRAINT_NAME FROM user_cons_columns WHERE TABLE_NAME = ', - wrapSingleQuote(tableName), + this.escape(tableName), ' and OWNER = ', - table.schema ? wrapSingleQuote(schemaName) : 'USER', + table.schema ? this.escape(schemaName) : 'USER', ' and COLUMN_NAME = ', - wrapSingleQuote(column), + this.escape(column), ' AND POSITION IS NOT NULL ORDER BY POSITION' ].join(''); @@ -1079,41 +1038,18 @@ class OracleQueryGenerator extends AbstractQueryGenerator { ' b.table_name "referencedTableName", b.column_name "referencedColumnName"', ' FROM all_cons_columns a', ' JOIN all_constraints c ON a.owner = c.owner AND a.constraint_name = c.constraint_name', - ' join all_cons_columns b on c.owner = b.owner and c.r_constraint_name = b.constraint_name', + ' JOIN all_cons_columns b ON c.owner = b.owner AND c.r_constraint_name = b.constraint_name', " WHERE c.constraint_type = 'R'", ' AND a.table_name = ', - wrapSingleQuote(tableName), + this.escape(tableName), ' AND a.owner = ', - table.schema ? wrapSingleQuote(schemaName) : 'USER', - ' order by a.table_name, a.constraint_name' + table.schema ? this.escape(schemaName) : 'USER', + ' ORDER BY a.table_name, a.constraint_name' ].join(''); return sql; } - quoteTable(param, as) { - let table = ''; - - if (_.isObject(param)) { - if (param.schema) { - table += `${this.quoteIdentifier(param.schema)}.`; - } - table += this.quoteIdentifier(param.tableName); - } else { - table = this.quoteIdentifier(param); - } - - // Oracle don't support as for table aliases - if (as) { - if (as.indexOf('.') > -1 || as.indexOf('_') === 0) { - table += ` ${this.quoteIdentifier(as, true)}`; - } else { - table += ` ${this.quoteIdentifier(as)}`; - } - } - return table; - } - nameIndexes(indexes, rawTablename) { let tableName; if (_.isObject(rawTablename)) { @@ -1122,15 +1058,14 @@ class OracleQueryGenerator extends AbstractQueryGenerator { tableName = rawTablename; } return _.map(indexes, index => { - if (!Object.prototype.hasOwnProperty.call(index, 'name')) { - if (index.unique) { - index.name = this._generateUniqueConstraintName(tableName, index.fields); - } else { - const onlyAttributeNames = index.fields.map(field => - typeof field === 'string' ? field : field.name || field.attribute - ); - index.name = Utils.underscore(`${tableName}_${onlyAttributeNames.join('_')}`); - } + if (Object.prototype.hasOwnProperty.call(index, 'name')) return; + if (index.unique) { + index.name = this._generateUniqueConstraintName(tableName, index.fields); + } else { + const onlyAttributeNames = index.fields.map(field => + typeof field === 'string' ? field : field.name || field.attribute + ); + index.name = Utils.underscore(`${tableName}_${onlyAttributeNames.join('_')}`); } return index; }); @@ -1147,9 +1082,9 @@ class OracleQueryGenerator extends AbstractQueryGenerator { 'FROM all_constraints cons, all_cons_columns cols ', 'INNER JOIN all_tab_columns atc ON(atc.table_name = cols.table_name AND atc.COLUMN_NAME = cols.COLUMN_NAME )', 'WHERE cols.table_name = ', - wrapSingleQuote(tableName), + this.escape(tableName), 'AND cols.owner = ', - table.schema ? wrapSingleQuote(schemaName) : 'USER ', + table.schema ? this.escape(schemaName) : 'USER ', "AND cons.constraint_type = 'P' ", 'AND cons.constraint_name = cols.constraint_name ', 'AND cons.owner = cols.owner ', @@ -1159,43 +1094,10 @@ class OracleQueryGenerator extends AbstractQueryGenerator { return sql; } - /** - * Request to know if the table has a identity primary key, returns the name of the declaration of the identity if true - * - * @param {object|string} table - */ - isIdentityPrimaryKey(table) { - const [tableName, schemaName] = this.getSchemaNameAndTableName(table); - return [ - 'SELECT TABLE_NAME,COLUMN_NAME, COLUMN_NAME,GENERATION_TYPE,IDENTITY_OPTIONS FROM DBA_TAB_IDENTITY_COLS WHERE TABLE_NAME = ', - wrapSingleQuote(tableName), - 'AND OWNER = ', - table.schema ? wrapSingleQuote(schemaName) : 'USER ' - ].join(''); - } - - /** - * Drop identity - * Mandatory, Oracle doesn't support dropping a PK column if it's an identity -> results in database corruption - * - * @param {object|string} tableName - * @param {string} columnName - */ - dropIdentityColumn(tableName, columnName) { - return `ALTER TABLE ${this.quoteTable(tableName)} MODIFY ${columnName} DROP IDENTITY`; - } - dropConstraintQuery(tableName, constraintName) { return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${constraintName}`; } - setAutocommitQuery(value) { - if (value) { - // Do nothing, just for eslint - } - return ''; - } - setIsolationLevelQuery(value, options) { if (options.parent) { return; @@ -1214,9 +1116,13 @@ class OracleQueryGenerator extends AbstractQueryGenerator { } } + getAliasToken() { + return ''; + } + startTransactionQuery(transaction) { if (transaction.parent) { - return `SAVEPOINT "${transaction.name}"`; + return `SAVEPOINT ${this.quoteIdentifier(transaction.name)}`; } return 'BEGIN TRANSACTION'; @@ -1232,23 +1138,12 @@ class OracleQueryGenerator extends AbstractQueryGenerator { rollbackTransactionQuery(transaction) { if (transaction.parent) { - return `ROLLBACK TO SAVEPOINT "${transaction.name}"`; + return `ROLLBACK TO SAVEPOINT ${this.quoteIdentifier(transaction.name)}`; } return 'ROLLBACK TRANSACTION'; } - selectFromTableFragment(options, model, attributes, tables, mainTableAs) { - this._throwOnEmptyAttributes(attributes, { modelName: model && model.name, as: mainTableAs }); - let mainFragment = `SELECT ${attributes.join(', ')} FROM ${tables}`; - - if (mainTableAs) { - mainFragment += ` ${mainTableAs}`; - } - - return mainFragment; - } - handleSequelizeMethod(smth, tableName, factory, options, prepend) { let str; if (smth instanceof Utils.Json) { @@ -1392,16 +1287,16 @@ class OracleQueryGenerator extends AbstractQueryGenerator { return value ? 1 : 0; } - quoteIdentifier(identifier, force) { - const optForceQuote = force || false; + quoteIdentifier(identifier, force = false) { + const optForceQuote = force; const optQuoteIdentifiers = this.options.quoteIdentifiers !== false; const rawIdentifier = Utils.removeTicks(identifier, '"'); - const regExp = (/^(([\w][\w\d_]*))$/g); + const regExp = /^(([\w][\w\d_]*))$/g; if ( optForceQuote !== true && optQuoteIdentifiers === false && - regExp.test(rawIdentifier) === true && + regExp.test(rawIdentifier) && !ORACLE_RESERVED_WORDS.includes(rawIdentifier.toUpperCase()) ) { // In Oracle, if tables, attributes or alias are created double-quoted, @@ -1418,7 +1313,7 @@ class OracleQueryGenerator extends AbstractQueryGenerator { * It causes bindbyPosition like :1, :2, :3 * We pass the val parameter so that the outBind indexes * starts after the inBind indexes end - * + * * @param {Array} bind * @param {number} posOffset */ @@ -1428,16 +1323,16 @@ class OracleQueryGenerator extends AbstractQueryGenerator { return `:${bind.length + posOffset}`; }; } -} -// private methods -function wrapSingleQuote(identifier) { - return Utils.addTicks(identifier, "'"); + /** + * Returns the authenticate test query string + */ + authTestQuery() { + return 'SELECT 1+1 AS result FROM DUAL'; + } } /* istanbul ignore next */ function throwMethodUndefined(methodName) { throw new Error(`The method "${methodName}" is not defined! Please add it to your sql dialect.`); } - -module.exports = OracleQueryGenerator; diff --git a/src/dialects/oracle/query-interface.js b/src/dialects/oracle/query-interface.js index 2ce9bcdc9696..ac188bbaae11 100644 --- a/src/dialects/oracle/query-interface.js +++ b/src/dialects/oracle/query-interface.js @@ -8,7 +8,7 @@ const _ = require('lodash'); /** * The interface that Sequelize uses to talk with Oracle database */ -class OracleQueryInterface extends QueryInterface { +export class OracleQueryInterface extends QueryInterface { /** * Upsert @@ -83,5 +83,3 @@ class OracleQueryInterface extends QueryInterface { return await this.sequelize.query(sql, options); } } - -exports.OracleQueryInterface = OracleQueryInterface; diff --git a/src/dialects/oracle/query.js b/src/dialects/oracle/query.js index 0dec9c953a15..50ad621c4f8c 100644 --- a/src/dialects/oracle/query.js +++ b/src/dialects/oracle/query.js @@ -11,7 +11,7 @@ const { logger } = require('../../utils/logger'); const debug = logger.debugContext('sql:oracle'); -class Query extends AbstractQuery { +export class OracleQuery extends AbstractQuery { constructor(connection, sequelize, options) { super(connection, sequelize, options); this.options = _.extend( @@ -32,32 +32,29 @@ class Query extends AbstractQuery { } getExecOptions() { - const self = this; - const execOpts = { outFormat: self.outFormat, autoCommit: self.autoCommit }; + const execOpts = { outFormat: this.outFormat, autoCommit: this.autoCommit }; // We set the oracledb - const oracledb = self.sequelize.connectionManager.lib; + const oracledb = this.sequelize.connectionManager.lib; if (this.isSelectQuery() && this.model) { const fInfo = {}; const keys = Object.keys(this.model.tableAttributes); - keys.forEach( key => { + for (const key of keys) { const keyValue = this.model.tableAttributes[key]; if ( keyValue.type.key === 'DECIMAL') { fInfo[key] = { type: oracledb.STRING }; } - }); - + } if ( fInfo ) { execOpts.fetchInfo = fInfo; } } return execOpts; } - _run(connection, sql, parameters) { - const self = this; + async _run(connection, sql, parameters) { // We set the oracledb - const oracledb = self.sequelize.connectionManager.lib; + const oracledb = this.sequelize.connectionManager.lib; // We remove the / that escapes quotes if (sql.match(/^(SELECT|INSERT|DELETE)/)) { this.sql = sql.replace(/; *$/, ''); @@ -71,7 +68,7 @@ class Query extends AbstractQuery { // When this.options.bindAttributes exists then it is an insertQuery/upsertQuery // So we insert the return bind direction and type - if (parameters !== undefined && this.options.outBindAttributes) { + if (this.options.outBindAttributes && (Array.isArray(parameters) || _.isPlainObject(parameters))) { outParameters.push(...Object.values(this.options.outBindAttributes)); // For upsertQuery we need to push the bindDef for isUpdate if (this.isUpsertQuery()) { @@ -82,7 +79,7 @@ class Query extends AbstractQuery { this.bindParameters = outParameters; // construct input binds from parameters for single row insert execute call // ex: [3, 4,...] - if (parameters !== undefined && typeof parameters === 'object') { + if (Array.isArray(parameters) || _.isPlainObject(parameters)) { if (this.options.executeMany) { // Constructing BindDefs for ExecuteMany call // Building the bindDef for in and out binds @@ -99,146 +96,127 @@ class Query extends AbstractQuery { } // TRANSACTION SUPPORT - if (_.startsWith(self.sql, 'BEGIN TRANSACTION')) { - self.autocommit = false; + if (this.sql.startsWith('BEGIN TRANSACTION')) { + this.autocommit = false; return Promise.resolve(); } - if (_.startsWith(self.sql, 'SET AUTOCOMMIT ON')) { - self.autocommit = true; + if (this.sql.startsWith('SET AUTOCOMMIT ON')) { + this.autocommit = true; return Promise.resolve(); } - if (_.startsWith(self.sql, 'SET AUTOCOMMIT OFF')) { - self.autocommit = false; + if (this.sql.startsWith('SET AUTOCOMMIT OFF')) { + this.autocommit = false; return Promise.resolve(); } - if (_.startsWith(self.sql, 'DECLARE x NUMBER')) { + if (this.sql.startsWith('DECLARE x NUMBER')) { // Calling a stored procedure for bulkInsert with NO attributes, returns nothing - if (self.autoCommit === undefined) { + if (this.autoCommit === undefined) { if (connection.uuid) { - self.autoCommit = false; + this.autoCommit = false; } else { - self.autoCommit = true; + this.autoCommit = true; } } - return connection - .execute(self.sql, this.bindParameters, { autoCommit: self.autoCommit }) - .then(() => { - return {}; - }) - .catch(error => { - throw self.formatError(error); - }) - .finally(() => { - complete(); - }); + + try { + await connection.execute(this.sql, this.bindParameters, { autoCommit: this.autoCommit }); + return Object.create(null); + } catch (error) { + throw this.formatError(error); + } finally { + complete(); + } } - if (_.startsWith(self.sql, 'BEGIN')) { + if (this.sql.startsWith('BEGIN')) { // Call to stored procedures - BEGIN TRANSACTION has been treated before - if (self.autoCommit === undefined) { + if (this.autoCommit === undefined) { if (connection.uuid) { - self.autoCommit = false; + this.autoCommit = false; } else { - self.autoCommit = true; + this.autoCommit = true; } } - return connection - .execute(self.sql, self.bindParameters, { - outFormat: self.outFormat, - autoCommit: self.autoCommit - }) - .then(result => { - if (!Array.isArray(result.outBinds)) { - return [result.outBinds]; - } - return result.outBinds; - }) - .catch(error => { - throw self.formatError(error); - }) - .finally(() => { - complete(); + try { + const result = await connection.execute(this.sql, this.bindParameters, { + outFormat: this.outFormat, + autoCommit: this.autoCommit }); + if (!Array.isArray(result.outBinds)) { + return [result.outBinds]; + } + return result.outBinds; + } catch (error) { + throw this.formatError(error); + } finally { + complete(); + } } - if (_.startsWith(self.sql, 'COMMIT TRANSACTION')) { - return connection - .commit() - .then(() => { - return {}; - }) - .catch(err => { - throw self.formatError(err); - }) - .finally(() => { - complete(); - }); + if (this.sql.startsWith('COMMIT TRANSACTION')) { + try { + await connection.commit(); + return Object.create(null); + } catch (error) { + throw this.formatError(error); + } finally { + complete(); + } } - if (_.startsWith(self.sql, 'ROLLBACK TRANSACTION')) { - return connection - .rollback() - .then(() => { - return {}; - }) - .catch(err => { - throw self.formatError(err); - }) - .finally(() => { - complete(); - }); + if (this.sql.startsWith('ROLLBACK TRANSACTION')) { + try { + await connection.rollback(); + return Object.create(null); + } catch (error) { + throw this.formatError(error); + } finally { + complete(); + } } - if (_.startsWith(self.sql, 'SET TRANSACTION')) { - return connection - .execute(self.sql, [], { autoCommit: false }) - .then(() => { - return {}; - }) - .catch(error => { - throw self.formatError(error); - }) - .finally(() => { - complete(); - }); + if (this.sql.startsWith('SET TRANSACTION')) { + try { + await connection.execute(this.sql, [], { autoCommit: false }); + return Object.create(null); + } catch (error) { + throw this.formatError(error); + } finally { + complete(); + } } // QUERY SUPPORT // As Oracle does everything in transaction, if autoCommit is not defined, we set it to true - if (self.autoCommit === undefined) { + if (this.autoCommit === undefined) { if (connection.uuid) { - self.autoCommit = false; + this.autoCommit = false; } else { - self.autoCommit = true; + this.autoCommit = true; } } // inbind parameters added byname. merge them - if ('inputParameters' in self.options && self.options.inputParameters !== null) { - Object.assign(self.bindParameters, self.options.inputParameters); + if ('inputParameters' in this.options && this.options.inputParameters !== null) { + Object.assign(this.bindParameters, this.options.inputParameters); } const execOpts = this.getExecOptions(); if (this.options.executeMany && bindDef.length > 0) { execOpts.bindDefs = bindDef; } - const executePromise = this.options.executeMany ? connection.executeMany(self.sql, self.bindParameters, execOpts) : connection.execute(self.sql, self.bindParameters, execOpts); - // If we have some mapping with parameters to do - INSERT queries - return executePromise - .then(result => { - return self.formatResults(result); - }) - .catch(error => { - throw self.formatError(error); - }) - .finally(() => { - complete(); - }); - + const executePromise = this.options.executeMany ? connection.executeMany(this.sql, this.bindParameters, execOpts) : connection.execute(this.sql, this.bindParameters, execOpts); + try { + const result = await executePromise; + return this.formatResults(result); + } catch (error) { + throw this.formatError(error); + } finally { + complete(); + } } run(sql, parameters) { - const self = this; if (!sql.match(/END;$/)) { sql = sql.replace(/; *$/, ''); } - return self._run(this.connection, sql, parameters); + return this._run(this.connection, sql, parameters); } /** @@ -622,10 +600,10 @@ class Query extends AbstractQuery { collate: undefined }); }); - const returnIndexes = []; + const returnIndexes = []; const accKeys = Object.keys(acc); - accKeys.forEach(accKey => { + for (const accKey of accKeys) { const columns = {}; columns.fields = acc[accKey].fields; // We are generating index field name in the format sequelize expects @@ -634,8 +612,7 @@ class Query extends AbstractQuery { acc[accKey].name = Utils.nameIndex(columns, acc[accKey].tableName).name; } returnIndexes.push(acc[accKey]); - }); - + } return returnIndexes; } @@ -666,7 +643,3 @@ class Query extends AbstractQuery { } } } - -module.exports = Query; -module.exports.Query = Query; -module.exports.default = Query; diff --git a/src/sequelize.js b/src/sequelize.js index 5ea8d7a821c0..538d454e3d82 100644 --- a/src/sequelize.js +++ b/src/sequelize.js @@ -963,12 +963,7 @@ class Sequelize { ...options }; - let sql = 'SELECT 1+1 AS result'; - if (this.dialect.name === 'oracle') { - sql += ' FROM DUAL'; - } - - await this.query(sql, options); + await this.query(this.dialect.queryGenerator.authTestQuery(), options); return; } diff --git a/src/sql-string.js b/src/sql-string.js index c7146478e5ab..3e37167c7754 100644 --- a/src/sql-string.js +++ b/src/sql-string.js @@ -74,13 +74,11 @@ function escape(val, timeZone, dialect, format) { // null character is not allowed in Postgres val = val.replace(/\0/g, '\\0'); } - } else if (dialect === 'oracle' && (val.indexOf('TO_TIMESTAMP') > -1 || val.indexOf('TO_DATE') > -1 || typeof val === 'string')) { - if (val.indexOf('TO_TIMESTAMP') > -1 || val.indexOf('TO_DATE') > -1) { + } else if (dialect === 'oracle' && typeof val === 'string') { + if (val.startsWith('TO_TIMESTAMP') || val.startsWith('TO_DATE')) { return val; - } - if (typeof val === 'string') { - val = val.replace(/'/g, "''"); } + val = val.replace(/'/g, "''"); } else { // eslint-disable-next-line no-control-regex diff --git a/test/integration/configuration.test.js b/test/integration/configuration.test.js index f468b69e6604..888066e86605 100644 --- a/test/integration/configuration.test.js +++ b/test/integration/configuration.test.js @@ -60,13 +60,10 @@ describe(Support.getTestDialectTeaser('Configuration'), () => { if (dialect === 'sqlite') { // SQLite doesn't require authentication and `select 1 as hello` is a valid query, so this should be fulfilled not rejected for it. await expect(seq.query('select 1 as hello')).to.eventually.be.fulfilled; - } - - else if (dialect === 'db2') { + } else if (dialect === 'db2') { await expect(seq.query('select 1 as hello')).to.eventually.be.rejectedWith(...willBeRejectedWithArgs); - } - else if (dialect === 'oracle') { - await expect(seq.query('select 1 as hello FROM DUAL')).to.eventually.be.rejectedWith(Sequelize.ConnectionRefusedError); + } else if (dialect === 'oracle') { + await expect(seq.query('select 1 as hello FROM DUAL')).to.eventually.be.rejectedWith(Sequelize.HostNotReachableError); } else { await expect(seq.query('select 1 as hello')).to.eventually.be.rejectedWith(Sequelize.ConnectionRefusedError, 'connect ECONNREFUSED'); } diff --git a/test/integration/json.test.js b/test/integration/json.test.js index 7e7de96bea10..931470cf57fd 100644 --- a/test/integration/json.test.js +++ b/test/integration/json.test.js @@ -27,7 +27,9 @@ describe('model', () => { const table = await this.sequelize.queryInterface.describeTable('Users'); // expected for mariadb 10.4 : https://jira.mariadb.org/browse/MDEV-15558 // oracledb 19c doesn't support JSON and the DB datatype is BLOB - if (dialect !== 'mariadb' && dialect !== 'oracle') { + if (dialect === 'oracle') { + expect(table.emergency_contact.type).to.equal('BLOB'); + } else if (dialect !== 'mariadb') { expect(table.emergency_contact.type).to.equal('JSON'); } }); diff --git a/test/integration/model.test.js b/test/integration/model.test.js index 5ea60480d4e2..c343931fe621 100644 --- a/test/integration/model.test.js +++ b/test/integration/model.test.js @@ -233,14 +233,14 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(user.deletedAtThisTime).to.exist; }); - // The Oracle dialect doesn't support empty string in a non-null column - (dialect !== 'oracle' ? it : it.skip)('returns proper defaultValues after save when setter is set', async function() { + it('returns proper defaultValues after save when setter is set', async function() { const titleSetter = sinon.spy(), Task = this.sequelize.define('TaskBuild', { title: { type: Sequelize.STRING(50), allowNull: false, - defaultValue: '' + // Oracle dialect doesn't support empty string in a non-null column + defaultValue: dialect === 'oracle' ? 'A' : '' } }, { setterMethods: { @@ -251,7 +251,11 @@ describe(Support.getTestDialectTeaser('Model'), () => { await Task.sync({ force: true }); const record = await Task.build().save(); expect(record.title).to.be.a('string'); - expect(record.title).to.equal(''); + if (dialect === 'oracle') { + expect(record.title).to.equal('A'); + } else { + expect(record.title).to.equal(''); + } expect(titleSetter.notCalled).to.be.ok; // The setter method should not be invoked for default values }); diff --git a/test/integration/model/findAll/order.test.js b/test/integration/model/findAll/order.test.js index adddecdcc169..894f0530938a 100644 --- a/test/integration/model/findAll/order.test.js +++ b/test/integration/model/findAll/order.test.js @@ -22,6 +22,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { }); }); + // Oracle doesn't support operators in Order by clause if (current.dialect.name !== 'mssql' && current.dialect.name !== 'oracle') { const email = current.dialect.name === 'db2' ? '"email"' : 'email'; it('should work with order: literal()', async function() { diff --git a/test/integration/query-interface.test.js b/test/integration/query-interface.test.js index 932446804651..126b7941ff82 100644 --- a/test/integration/query-interface.test.js +++ b/test/integration/query-interface.test.js @@ -40,18 +40,15 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { await sequelize.query('DROP VIEW V_Fail'); } else if (dialect === 'oracle') { const plsql = [ - 'DECLARE', - ' V_COUNT INTEGER;', 'BEGIN', - ' V_COUNT := 0;', - ' SELECT COUNT(1) INTO V_COUNT FROM USER_VIEWS WHERE VIEW_NAME = \'V_FAIL\';', - ' IF V_COUNT != 0 THEN', - ' EXECUTE IMMEDIATE ', - '\'DROP VIEW V_Fail\'', - ';', + 'EXECUTE IMMEDIATE', + '\'DROP VIEW V_Fail\';', + 'EXCEPTION WHEN OTHERS THEN', + ' IF SQLCODE != -942 THEN', + ' RAISE;', ' END IF;', 'END;' - ].join(''); + ].join(' '); await sequelize.query(plsql); } else { await sequelize.query('DROP VIEW IF EXISTS V_Fail'); @@ -648,7 +645,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { field: 'username' }, onDelete: 'cascade', - onUpdate: dialect !== 'oracle' ? 'cascade' : null, + onUpdate: dialect !== 'oracle' ? 'cascade' : null, type: 'foreign key' }); let constraints = await this.queryInterface.showConstraint('posts'); diff --git a/test/integration/sequelize/query.test.js b/test/integration/sequelize/query.test.js index 0063eaec886a..32a0039a4857 100644 --- a/test/integration/sequelize/query.test.js +++ b/test/integration/sequelize/query.test.js @@ -63,16 +63,30 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { await this.sequelize.query(this.insertQuery); }); - // Oracle dialect doesn't support insert of multiple rows using insert into statement - // INSERT ALL INTO statement can be used instead - (dialect !== 'oracle' ? it : it.skip)('executes a query if a placeholder value is an array', async function() { - await this.sequelize.query(`INSERT INTO ${qq(this.User.tableName)} (${qq('username')}, ${qq('email_address')}, ` + - `${qq('createdAt')}, ${qq('updatedAt')}) VALUES ?;`, { - replacements: [[ - ['john', 'john@gmail.com', '2012-01-01 10:10:10', '2012-01-01 10:10:10'], - ['michael', 'michael@gmail.com', '2012-01-01 10:10:10', '2012-01-01 10:10:10'] - ]] - }); + it('executes a query if a placeholder value is an array', async function() { + if (dialect === 'oracle') { + await this.sequelize.query( + 'INSERT ' + + `INTO ${qq(this.User.tableName)} (${qq('username')}, ${qq('email_address')}, ` + + `${qq('createdAt')}, ${qq('updatedAt')}) ` + + `with p (${qq('username')}, ${qq('email_address')}, ${qq('createdAt')}, ${qq('updatedAt')}) as ( ` + + 'select ? from dual union all ' + + 'select ? from dual ' + + ') select * from p; ', { + replacements: [ + ['john', 'john@gmail.com', new Date('2012-01-01 10:10:10'), new Date('2012-01-01 10:10:10')], + ['michael', 'michael@gmail.com', new Date('2012-01-01 10:10:10'), new Date('2012-01-01 10:10:10')] + ] + }); + } else { + await this.sequelize.query(`INSERT INTO ${qq(this.User.tableName)} (${qq('username')}, ${qq('email_address')}, ` + + `${qq('createdAt')}, ${qq('updatedAt')}) VALUES ?;`, { + replacements: [[ + ['john', 'john@gmail.com', '2012-01-01 10:10:10', '2012-01-01 10:10:10'], + ['michael', 'michael@gmail.com', '2012-01-01 10:10:10', '2012-01-01 10:10:10'] + ]] + }); + } const rows = await this.sequelize.query(`SELECT * FROM ${qq(this.User.tableName)};`, { type: this.sequelize.QueryTypes.SELECT diff --git a/test/integration/timezone.test.js b/test/integration/timezone.test.js index a4a915842487..b8ab03f88eb0 100644 --- a/test/integration/timezone.test.js +++ b/test/integration/timezone.test.js @@ -29,9 +29,7 @@ if (dialect !== 'sqlite') { let query = `SELECT ${now} as now`; if (dialect === 'db2') { query = `SELECT ${now} as "now"`; - } - - if (dialect === 'oracle') { + } else if (dialect === 'oracle') { query = 'SELECT sysdate AS "now" FROM DUAL'; } diff --git a/test/unit/sql/change-column.test.js b/test/unit/sql/change-column.test.js index 16155f8f6603..e6a89d5ddcbf 100644 --- a/test/unit/sql/change-column.test.js +++ b/test/unit/sql/change-column.test.js @@ -61,7 +61,7 @@ if (current.dialect.name !== 'sqlite') { onDelete: 'cascade' }).then(sql => { expectsql(sql, { - oracle: 'DECLARE CONS_NAME VARCHAR2(200); BEGIN BEGIN select constraint_name into cons_name from ( select distinct cc.owner, cc.table_name, cc.constraint_name, cc.column_name as cons_columns from all_cons_columns cc, all_constraints c where cc.owner = c.owner and cc.table_name = c.table_name and cc.constraint_name = c.constraint_name and c.constraint_type = \'R\' group by cc.owner, cc.table_name, cc.constraint_name, cc.column_name ) where owner = USER and table_name = \'users\' and cons_columns = \'level_id\' ; EXCEPTION WHEN NO_DATA_FOUND THEN CONS_NAME := NULL; END; IF CONS_NAME IS NOT NULL THEN EXECUTE IMMEDIATE \'ALTER TABLE "users" DROP CONSTRAINT "\'||CONS_NAME||\'"\'; END IF; EXECUTE IMMEDIATE \'ALTER TABLE "users" ADD FOREIGN KEY ("level_id") REFERENCES "level" ("id") ON DELETE CASCADE\'; END;', + oracle: 'DECLARE CONS_NAME VARCHAR2(200); BEGIN BEGIN SELECT constraint_name INTO cons_name FROM (SELECT DISTINCT cc.owner, cc.table_name, cc.constraint_name, cc.column_name AS cons_columns FROM all_cons_columns cc, all_constraints c WHERE cc.owner = c.owner AND cc.table_name = c.table_name AND cc.constraint_name = c.constraint_name AND c.constraint_type = \'R\' GROUP BY cc.owner, cc.table_name, cc.constraint_name, cc.column_name) WHERE owner = USER AND table_name = \'users\' AND cons_columns = \'level_id\' ; EXCEPTION WHEN NO_DATA_FOUND THEN CONS_NAME := NULL; END; IF CONS_NAME IS NOT NULL THEN EXECUTE IMMEDIATE \'ALTER TABLE "users" DROP CONSTRAINT "\'||CONS_NAME||\'"\'; END IF; EXECUTE IMMEDIATE \'ALTER TABLE "users" ADD FOREIGN KEY ("level_id") REFERENCES "level" ("id") ON DELETE CASCADE\'; END;', mssql: 'ALTER TABLE [users] ADD FOREIGN KEY ([level_id]) REFERENCES [level] ([id]) ON DELETE CASCADE;', db2: 'ALTER TABLE "users" ADD CONSTRAINT "level_id_foreign_idx" FOREIGN KEY ("level_id") REFERENCES "level" ("id") ON DELETE CASCADE;', mariadb: 'ALTER TABLE `users` ADD FOREIGN KEY (`level_id`) REFERENCES `level` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;', diff --git a/test/unit/sql/create-table.test.js b/test/unit/sql/create-table.test.js index 115259520239..0cdf47ff0974 100644 --- a/test/unit/sql/create-table.test.js +++ b/test/unit/sql/create-table.test.js @@ -27,7 +27,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'CREATE TABLE IF NOT EXISTS "foo"."users" ("id" SERIAL , "mood" "foo"."enum_users_mood", PRIMARY KEY ("id"));', mariadb: "CREATE TABLE IF NOT EXISTS `foo`.`users` (`id` INTEGER NOT NULL auto_increment , `mood` ENUM('happy', 'sad'), PRIMARY KEY (`id`)) ENGINE=InnoDB;", mysql: "CREATE TABLE IF NOT EXISTS `foo.users` (`id` INTEGER NOT NULL auto_increment , `mood` ENUM('happy', 'sad'), PRIMARY KEY (`id`)) ENGINE=InnoDB;", - oracle: 'BEGIN EXECUTE IMMEDIATE \'CREATE TABLE "foo"."users" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY, "mood" VARCHAR2(512),CONSTRAINT "PK_foo.users_id" PRIMARY KEY ("id"), CHECK ("mood" IN(\'\'happy\'\', \'\'sad\'\')))\'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;', + oracle: 'BEGIN EXECUTE IMMEDIATE \'CREATE TABLE "foo"."users" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY, "mood" VARCHAR2(512) CHECK ("mood" IN(\'\'happy\'\', \'\'sad\'\')),CONSTRAINT "PK_foo.users_id" PRIMARY KEY ("id"))\'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;', mssql: "IF OBJECT_ID('[foo].[users]', 'U') IS NULL CREATE TABLE [foo].[users] ([id] INTEGER NOT NULL IDENTITY(1,1) , [mood] VARCHAR(255) CHECK ([mood] IN(N'happy', N'sad')), PRIMARY KEY ([id]));" }); }); diff --git a/test/unit/sql/select.test.js b/test/unit/sql/select.test.js index 290aae12084a..0fc49930ceed 100644 --- a/test/unit/sql/select.test.js +++ b/test/unit/sql/select.test.js @@ -130,9 +130,9 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] } }, { - oracle: 'SELECT "user".* FROM (' + - 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 1 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + - 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 5 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub)' + + oracle: 'SELECT "user".* FROM (' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 1 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 5 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub)' + ' "user" ORDER BY "subquery_order_0" ASC;', default: `SELECT [user].* FROM (${ [ @@ -174,7 +174,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[last_name] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 1 AND [project_users].[status] = 1 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub`, `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[last_name] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 5 AND [project_users].[status] = 1 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub` ].join(current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ') - }) AS [user] ORDER BY [subquery_order_0] ASC;` + }) AS [user] ORDER BY [subquery_order_0] ASC;` }); testsql({ @@ -200,7 +200,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] } }, { - oracle: 'SELECT "user".* FROM (' + + oracle: 'SELECT "user".* FROM (' + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."id_user" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 1 WHERE "user"."age" >= 21 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."id_user" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 5 WHERE "user"."age" >= 21 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub) ' + '"user" ORDER BY "subquery_order_0" ASC;', @@ -770,8 +770,9 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }).include, model: User }, User), { - oracle: 'SELECT "User"."name", "User"."age", "Posts"."id" AS "Posts.id", "Posts"."* FROM User; DELETE FROM User;SELECT id" AS "Posts.* FROM User; DELETE FROM User;SELECT id" FROM "User" "User" LEFT OUTER JOIN "Post" "Posts" ON "User"."id" = "Posts"."user_id";', - default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[* FROM User; DELETE FROM User;SELECT id] AS [Posts.* FROM User; DELETE FROM User;SELECT id] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];' }); + default: 'SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[* FROM User; DELETE FROM User;SELECT id] AS [Posts.* FROM User; DELETE FROM User;SELECT id] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];', + oracle: 'SELECT "User"."name", "User"."age", "Posts"."id" AS "Posts.id", "Posts"."* FROM User; DELETE FROM User;SELECT id" AS "Posts.* FROM User; DELETE FROM User;SELECT id" FROM "User" "User" LEFT OUTER JOIN "Post" "Posts" ON "User"."id" = "Posts"."user_id";' + }); expectsql(sql.selectQuery('User', { attributes: ['name', 'age'], From 42ff1296e5ac905c7747bfbee2b59ef373b38a4e Mon Sep 17 00:00:00 2001 From: Sudarshan Soma <48428602+sudarshan12s@users.noreply.github.com> Date: Mon, 27 Jun 2022 22:52:43 +0530 Subject: [PATCH 03/14] fix: minor fixes done (#9) * fix: minor fixes to the review comments --- .../{latest => 21-slim}/docker-compose.yml | 0 dev/oracle/{latest => 21-slim}/privileges.sql | 0 dev/oracle/{latest => 21-slim}/start.sh | 0 dev/oracle/{latest => 21-slim}/stop.sh | 0 .../{latest => 21-slim}/wait-until-healthy.sh | 0 package.json | 4 +- src/dialects/oracle/data-types.js | 2 +- src/dialects/oracle/query-generator.js | 7 ++- src/dialects/oracle/query.js | 46 +++++++--------- .../associations/belongs-to.test.js | 4 +- .../integration/associations/has-many.test.js | 2 +- test/integration/associations/has-one.test.js | 4 +- test/integration/data-types.test.js | 5 +- test/integration/error.test.js | 2 +- test/integration/include/findAll.test.js | 18 +++--- test/integration/include/schema.test.js | 21 +++---- test/integration/model.test.js | 10 ++-- test/integration/model/bulk-create.test.js | 2 +- test/integration/model/create.test.js | 2 +- test/integration/model/findAll/order.test.js | 4 +- test/integration/query-interface.test.js | 2 +- .../query-interface/changeColumn.test.js | 3 +- test/integration/sequelize/query.test.js | 28 +++++----- test/integration/utils.test.js | 6 +- test/unit/sql/generateJoin.test.js | 55 +++++++++---------- test/unit/sql/offset-limit.test.js | 2 +- test/unit/sql/select.test.js | 30 +++++----- test/unit/sql/where.test.js | 1 - 28 files changed, 121 insertions(+), 139 deletions(-) rename dev/oracle/{latest => 21-slim}/docker-compose.yml (100%) rename dev/oracle/{latest => 21-slim}/privileges.sql (100%) rename dev/oracle/{latest => 21-slim}/start.sh (100%) rename dev/oracle/{latest => 21-slim}/stop.sh (100%) rename dev/oracle/{latest => 21-slim}/wait-until-healthy.sh (100%) diff --git a/dev/oracle/latest/docker-compose.yml b/dev/oracle/21-slim/docker-compose.yml similarity index 100% rename from dev/oracle/latest/docker-compose.yml rename to dev/oracle/21-slim/docker-compose.yml diff --git a/dev/oracle/latest/privileges.sql b/dev/oracle/21-slim/privileges.sql similarity index 100% rename from dev/oracle/latest/privileges.sql rename to dev/oracle/21-slim/privileges.sql diff --git a/dev/oracle/latest/start.sh b/dev/oracle/21-slim/start.sh similarity index 100% rename from dev/oracle/latest/start.sh rename to dev/oracle/21-slim/start.sh diff --git a/dev/oracle/latest/stop.sh b/dev/oracle/21-slim/stop.sh similarity index 100% rename from dev/oracle/latest/stop.sh rename to dev/oracle/21-slim/stop.sh diff --git a/dev/oracle/latest/wait-until-healthy.sh b/dev/oracle/21-slim/wait-until-healthy.sh similarity index 100% rename from dev/oracle/latest/wait-until-healthy.sh rename to dev/oracle/21-slim/wait-until-healthy.sh diff --git a/package.json b/package.json index 5a537d4e242a..f74a030b8350 100644 --- a/package.json +++ b/package.json @@ -244,14 +244,14 @@ "start-postgres": "bash dev/postgres/10/start.sh", "start-mssql": "bash dev/mssql/2019/start.sh", "start-db2": "bash dev/db2/11.5/start.sh", - "start-oracle": "bash dev/oracle/latest/start.sh", + "start-oracle": "bash dev/oracle/21-slim/start.sh", "stop-mariadb": "bash dev/mariadb/10.3/stop.sh", "stop-mysql": "bash dev/mysql/5.7/stop.sh", "stop-mysql-8": "bash dev/mysql/8.0/stop.sh", "stop-postgres": "bash dev/postgres/10/stop.sh", "stop-mssql": "bash dev/mssql/2019/stop.sh", "stop-db2": "bash dev/db2/11.5/stop.sh", - "stop-oracle": "bash dev/oracle/latest/stop.sh", + "stop-oracle": "bash dev/oracle/21-slim/stop.sh", "restart-mariadb": "npm run start-mariadb", "restart-mysql": "npm run start-mysql", "restart-postgres": "npm run start-postgres", diff --git a/src/dialects/oracle/data-types.js b/src/dialects/oracle/data-types.js index c8807a2ff910..4b6e06ca236b 100644 --- a/src/dialects/oracle/data-types.js +++ b/src/dialects/oracle/data-types.js @@ -145,7 +145,7 @@ module.exports = BaseTypes => { class CHAR extends BaseTypes.CHAR { toSql() { if (this._binary) { - warn('CHAR.BINARY datatype is not of Fixed Length.'); + warn('Oracle CHAR.BINARY datatype is not of Fixed Length.'); return `RAW(${this._length})`; } return super.toSql(); diff --git a/src/dialects/oracle/query-generator.js b/src/dialects/oracle/query-generator.js index cfdee9634be9..a020a8510b4b 100644 --- a/src/dialects/oracle/query-generator.js +++ b/src/dialects/oracle/query-generator.js @@ -1266,9 +1266,10 @@ export class OracleQueryGenerator extends AbstractQueryGenerator { } if (options.limit || options.offset) { - if (!(options.order && options.group) && (!options.order || options.include && !orders.subQueryOrder.length)) { - fragment += options.order && !isSubQuery ? ', ' : ' ORDER BY '; - fragment += `${this.quoteTable(options.tableAs || model.name) }.${this.quoteIdentifier(model.primaryKeyField)}`; + // Add needed order by clause only when it is not provided + if (!orders.mainQueryOrder?.length || isSubQuery && !orders.subQueryOrder?.length) { + const tablePkFragment = `${this.quoteTable(options.tableAs || model.name)}.${this.quoteIdentifier(model.primaryKeyField)}`; + fragment += ` ORDER BY ${tablePkFragment}`; } if (options.offset || options.limit) { diff --git a/src/dialects/oracle/query.js b/src/dialects/oracle/query.js index 50ad621c4f8c..b803c632a311 100644 --- a/src/dialects/oracle/query.js +++ b/src/dialects/oracle/query.js @@ -42,7 +42,11 @@ export class OracleQuery extends AbstractQuery { const keys = Object.keys(this.model.tableAttributes); for (const key of keys) { const keyValue = this.model.tableAttributes[key]; - if ( keyValue.type.key === 'DECIMAL') { + if (keyValue.type.key === 'DECIMAL') { + fInfo[key] = { type: oracledb.STRING }; + } + // Fetching BIGINT as string since, node-oracledb doesn't support JS BIGINT yet + if (keyValue.type.key === 'BIGINT') { fInfo[key] = { type: oracledb.STRING }; } } @@ -52,20 +56,20 @@ export class OracleQuery extends AbstractQuery { } return execOpts; } - async _run(connection, sql, parameters) { + async run(sql, parameters) { // We set the oracledb const oracledb = this.sequelize.connectionManager.lib; - // We remove the / that escapes quotes - if (sql.match(/^(SELECT|INSERT|DELETE)/)) { - this.sql = sql.replace(/; *$/, ''); - } else { - this.sql = sql; - } const complete = this._logQuery(sql, debug, parameters); const outParameters = []; const bindParameters = []; const bindDef = []; + if (!sql.match(/END;$/)) { + this.sql = sql.replace(/; *$/, ''); + } else { + this.sql = sql; + } + // When this.options.bindAttributes exists then it is an insertQuery/upsertQuery // So we insert the return bind direction and type if (this.options.outBindAttributes && (Array.isArray(parameters) || _.isPlainObject(parameters))) { @@ -111,7 +115,7 @@ export class OracleQuery extends AbstractQuery { if (this.sql.startsWith('DECLARE x NUMBER')) { // Calling a stored procedure for bulkInsert with NO attributes, returns nothing if (this.autoCommit === undefined) { - if (connection.uuid) { + if (this.connection.uuid) { this.autoCommit = false; } else { this.autoCommit = true; @@ -119,7 +123,7 @@ export class OracleQuery extends AbstractQuery { } try { - await connection.execute(this.sql, this.bindParameters, { autoCommit: this.autoCommit }); + await this.connection.execute(this.sql, this.bindParameters, { autoCommit: this.autoCommit }); return Object.create(null); } catch (error) { throw this.formatError(error); @@ -130,7 +134,7 @@ export class OracleQuery extends AbstractQuery { if (this.sql.startsWith('BEGIN')) { // Call to stored procedures - BEGIN TRANSACTION has been treated before if (this.autoCommit === undefined) { - if (connection.uuid) { + if (this.connection.uuid) { this.autoCommit = false; } else { this.autoCommit = true; @@ -138,7 +142,7 @@ export class OracleQuery extends AbstractQuery { } try { - const result = await connection.execute(this.sql, this.bindParameters, { + const result = await this.connection.execute(this.sql, this.bindParameters, { outFormat: this.outFormat, autoCommit: this.autoCommit }); @@ -154,7 +158,7 @@ export class OracleQuery extends AbstractQuery { } if (this.sql.startsWith('COMMIT TRANSACTION')) { try { - await connection.commit(); + await this.connection.commit(); return Object.create(null); } catch (error) { throw this.formatError(error); @@ -164,7 +168,7 @@ export class OracleQuery extends AbstractQuery { } if (this.sql.startsWith('ROLLBACK TRANSACTION')) { try { - await connection.rollback(); + await this.connection.rollback(); return Object.create(null); } catch (error) { throw this.formatError(error); @@ -174,7 +178,7 @@ export class OracleQuery extends AbstractQuery { } if (this.sql.startsWith('SET TRANSACTION')) { try { - await connection.execute(this.sql, [], { autoCommit: false }); + await this.connection.execute(this.sql, [], { autoCommit: false }); return Object.create(null); } catch (error) { throw this.formatError(error); @@ -185,7 +189,7 @@ export class OracleQuery extends AbstractQuery { // QUERY SUPPORT // As Oracle does everything in transaction, if autoCommit is not defined, we set it to true if (this.autoCommit === undefined) { - if (connection.uuid) { + if (this.connection.uuid) { this.autoCommit = false; } else { this.autoCommit = true; @@ -200,7 +204,7 @@ export class OracleQuery extends AbstractQuery { if (this.options.executeMany && bindDef.length > 0) { execOpts.bindDefs = bindDef; } - const executePromise = this.options.executeMany ? connection.executeMany(this.sql, this.bindParameters, execOpts) : connection.execute(this.sql, this.bindParameters, execOpts); + const executePromise = this.options.executeMany ? this.connection.executeMany(this.sql, this.bindParameters, execOpts) : this.connection.execute(this.sql, this.bindParameters, execOpts); try { const result = await executePromise; return this.formatResults(result); @@ -211,14 +215,6 @@ export class OracleQuery extends AbstractQuery { } } - run(sql, parameters) { - if (!sql.match(/END;$/)) { - sql = sql.replace(/; *$/, ''); - } - - return this._run(this.connection, sql, parameters); - } - /** * The parameters to query.run function are built here * diff --git a/test/integration/associations/belongs-to.test.js b/test/integration/associations/belongs-to.test.js index 163fffc32a4d..011d7ac18cfb 100644 --- a/test/integration/associations/belongs-to.test.js +++ b/test/integration/associations/belongs-to.test.js @@ -625,9 +625,7 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { } // NOTE: mssql does not support changing an autoincrement primary key - if (Support.getTestDialect() !== 'mssql' && - Support.getTestDialect() !== 'db2' && - Support.getTestDialect() !== 'oracle') { + if (!['mssql', 'db2', 'oracle'].includes(Support.getTestDialect())) { it('can cascade updates', async function() { const Task = this.sequelize.define('Task', { title: DataTypes.STRING }), User = this.sequelize.define('User', { username: DataTypes.STRING }); diff --git a/test/integration/associations/has-many.test.js b/test/integration/associations/has-many.test.js index 4d3a1408ce10..114ebe596708 100644 --- a/test/integration/associations/has-many.test.js +++ b/test/integration/associations/has-many.test.js @@ -1102,7 +1102,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { }); // NOTE: mssql does not support changing an autoincrement primary key - if (dialect !== 'mssql' && dialect !== 'db2' && dialect !== 'oracle') { + if (!['mssql', 'db2', 'oracle'].includes(dialect)) { it('can cascade updates', async function() { const Task = this.sequelize.define('Task', { title: DataTypes.STRING }), User = this.sequelize.define('User', { username: DataTypes.STRING }); diff --git a/test/integration/associations/has-one.test.js b/test/integration/associations/has-one.test.js index 5535db9fa7c6..3f4a430fe574 100644 --- a/test/integration/associations/has-one.test.js +++ b/test/integration/associations/has-one.test.js @@ -453,9 +453,7 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { }); // NOTE: mssql does not support changing an autoincrement primary key - if (Support.getTestDialect() !== 'mssql' && - Support.getTestDialect() !== 'db2' && - Support.getTestDialect() !== 'oracle') { + if (!['mssql', 'db2', 'oracle'].includes(Support.getTestDialect())) { it('can cascade updates', async function() { const Task = this.sequelize.define('Task', { title: Sequelize.STRING }), User = this.sequelize.define('User', { username: Sequelize.STRING }); diff --git a/test/integration/data-types.test.js b/test/integration/data-types.test.js index cb2430a533ec..bef10cea23ab 100644 --- a/test/integration/data-types.test.js +++ b/test/integration/data-types.test.js @@ -230,7 +230,7 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { // mssql/oracle has a _bindParam function that checks if STRING was created with // the boolean param (if so it outputs a Buffer bind param). This override // isn't needed for other dialects - if (dialect === 'mssql' || dialect === 'db2' || dialect === 'oracle') { + if (['mssql', 'db2', 'oracle'].includes(dialect)) { await testSuccess(Type, 'foobar', { useBindParam: true }); } else { await testSuccess(Type, 'foobar'); @@ -277,8 +277,7 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => { } }); - // Node-oracledb doesn't support JS BigInt yet - (dialect !== 'oracle' ? it : it.skip)('should handle JS BigInt type', async function() { + it('should handle JS BigInt type', async function() { const User = this.sequelize.define('user', { age: Sequelize.BIGINT }); diff --git a/test/integration/error.test.js b/test/integration/error.test.js index 96821a3c6b95..31ce5fb6020f 100644 --- a/test/integration/error.test.js +++ b/test/integration/error.test.js @@ -374,7 +374,7 @@ describe(Support.getTestDialectTeaser('Sequelize Errors'), () => { await expect(User.create({ name: 'jan' })).to.be.rejectedWith(Sequelize.UniqueConstraintError); // And when the model is not passed at all - if (dialect === 'db2' || dialect === 'oracle') { + if (['db2', 'oracle'].includes(dialect)) { await expect(this.sequelize.query('INSERT INTO "users" ("name") VALUES (\'jan\')')).to.be.rejectedWith(Sequelize.UniqueConstraintError); } else { await expect(this.sequelize.query('INSERT INTO users (name) VALUES (\'jan\')')).to.be.rejectedWith(Sequelize.UniqueConstraintError); diff --git a/test/integration/include/findAll.test.js b/test/integration/include/findAll.test.js index 679c039c72ad..458f1ffc08b7 100644 --- a/test/integration/include/findAll.test.js +++ b/test/integration/include/findAll.test.js @@ -123,7 +123,7 @@ describe(Support.getTestDialectTeaser('Include'), () => { { title: 'Pen' }, { title: 'Monitor' } ]); - const products = await Product.findAll(); + const products = await Product.findAll({ order: [['id', 'ASC']] }); const groupMembers = [ { AccUserId: user.id, GroupId: groups[0].id, RankId: ranks[0].id }, { AccUserId: user.id, GroupId: groups[1].id, RankId: ranks[2].id } @@ -293,8 +293,7 @@ describe(Support.getTestDialectTeaser('Include'), () => { }); }); - // On update cascade not supported in the Oracle dialect - (dialect !== 'oracle' ? it : it.skip)('should support an include with multiple different association types', async function() { + it('should support an include with multiple different association types', async function() { const User = this.sequelize.define('User', {}), Product = this.sequelize.define('Product', { title: DataTypes.STRING @@ -361,7 +360,7 @@ describe(Support.getTestDialectTeaser('Include'), () => { Product.bulkCreate([ { title: 'Chair' }, { title: 'Desk' } - ]).then(() => Product.findAll()) + ]).then(() => Product.findAll({ order: [['id', 'ASC']] })) ]); await Promise.all([ GroupMember.bulkCreate([ @@ -1138,8 +1137,7 @@ describe(Support.getTestDialectTeaser('Include'), () => { expect(products[0].Tags.length).to.equal(1); }); - // On update cascade not supported in the Oracle dialect - (dialect !== 'oracle' ? it : it.skip)('should be possible to extend the on clause with a where option on nested includes', async function() { + it('should be possible to extend the on clause with a where option on nested includes', async function() { const User = this.sequelize.define('User', { name: DataTypes.STRING }), @@ -1210,7 +1208,7 @@ describe(Support.getTestDialectTeaser('Include'), () => { { title: 'Desk' } ]); - const products = await Product.findAll(); + const products = await Product.findAll({ order: [['id', 'ASC']] }); await Promise.all([ GroupMember.bulkCreate([ { UserId: user.id, GroupId: groups[0].id, RankId: ranks[0].id }, @@ -1309,8 +1307,7 @@ describe(Support.getTestDialectTeaser('Include'), () => { }); }); - // On update cascade not supported in the Oracle dialect - (dialect !== 'oracle' ? it : it.skip)('should be possible use limit, attributes and a where on a belongsTo with additional hasMany includes', async function() { + it('should be possible use limit, attributes and a where on a belongsTo with additional hasMany includes', async function() { await this.fixtureA(); const products = await this.models.Product.findAll({ @@ -1526,8 +1523,7 @@ describe(Support.getTestDialectTeaser('Include'), () => { }); }); - // On update cascade not supported in the Oracle dialect - (dialect !== 'oracle' ? it : it.skip)('should be possible to use limit and a where on a hasMany with a through model with additional includes', async function() { + it('should be possible to use limit and a where on a hasMany with a through model with additional includes', async function() { await this.fixtureA(); const products = await this.models.Product.findAll({ diff --git a/test/integration/include/schema.test.js b/test/integration/include/schema.test.js index d18c6a4a1060..b8d7c26bb6a5 100644 --- a/test/integration/include/schema.test.js +++ b/test/integration/include/schema.test.js @@ -127,7 +127,7 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { { title: 'Bed' }, { title: 'Pen' }, { title: 'Monitor' } - ]).then(() => Product.findAll()) + ]).then(() => Product.findAll({ order: [['id', 'ASC']] })) ]); const groupMembers = [ { AccUserId: user.id, GroupId: groups[0].id, RankId: ranks[0].id }, @@ -179,8 +179,7 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { await this.sequelize.createSchema('account'); }); - // On update cascade not supported in the Oracle dialect - (dialect !== 'oracle' ? it : it.skip)('should support an include with multiple different association types', async function() { + it('should support an include with multiple different association types', async function() { await this.sequelize.dropSchema('account'); await this.sequelize.createSchema('account'); const AccUser = this.sequelize.define('AccUser', {}, { schema: 'account' }), @@ -249,7 +248,7 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { Product.bulkCreate([ { title: 'Chair' }, { title: 'Desk' } - ]).then(() => Product.findAll()) + ]).then(() => Product.findAll({ order: [['id', 'ASC']] })) ]); await Promise.all([ GroupMember.bulkCreate([ @@ -868,8 +867,7 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { expect(products[0].Tags.length).to.equal(1); }); - // On update cascade not supported in the Oracle dialect - (dialect !== 'oracle' ? it : it.skip)('should be possible to extend the on clause with a where option on nested includes', async function() { + it('should be possible to extend the on clause with a where option on nested includes', async function() { const User = this.sequelize.define('User', { name: DataTypes.STRING }, { schema: 'account' }), @@ -938,7 +936,7 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { Product.bulkCreate([ { title: 'Chair' }, { title: 'Desk' } - ]).then(() => Product.findAll()) + ]).then(() => Product.findAll({ order: [['id', 'ASC']] })) ]); await Promise.all([ GroupMember.bulkCreate([ @@ -1038,8 +1036,7 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { }); }); - // On update cascade not supported in the Oracle dialect - (dialect !== 'oracle' ? it : it.skip)('should be possible use limit, attributes and a where on a belongsTo with additional hasMany includes', async function() { + it('should be possible use limit, attributes and a where on a belongsTo with additional hasMany includes', async function() { await this.fixtureA(); const products = await this.models.Product.findAll({ @@ -1064,8 +1061,7 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { }); }); - // On update cascade not supported in the Oracle dialect - (dialect !== 'oracle' ? it : it.skip)('should be possible to use limit and a where on a hasMany with additional includes', async function() { + it('should be possible to use limit and a where on a hasMany with additional includes', async function() { await this.fixtureA(); const products = await this.models.Product.findAll({ @@ -1094,8 +1090,7 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { }); }); - // On update cascade not supported in the Oracle dialect - (dialect !== 'oracle' ? it : it.skip)('should be possible to use limit and a where on a hasMany with a through model with additional includes', async function() { + it('should be possible to use limit and a where on a hasMany with a through model with additional includes', async function() { await this.fixtureA(); const products = await this.models.Product.findAll({ diff --git a/test/integration/model.test.js b/test/integration/model.test.js index c343931fe621..283a38c4f450 100644 --- a/test/integration/model.test.js +++ b/test/integration/model.test.js @@ -1012,7 +1012,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { fields: ['secretValue'], logging(sql) { test = true; - if (dialect === 'mssql' || dialect === 'oracle') { + if (['mssql', 'oracle'].includes(dialect)) { expect(sql).to.not.contain('createdAt'); } else { expect(sql).to.match(/UPDATE\s+[`"]+User1s[`"]+\s+SET\s+[`"]+secretValue[`"]=(\$1|\?),[`"]+updatedAt[`"]+=(\$2|\?)\s+WHERE [`"]+id[`"]+\s=\s(\$3|\?)/); @@ -1650,15 +1650,15 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(await User.findOne({ where: { username: 'Bob' } })).to.be.null; const tobi = await User.findOne({ where: { username: 'Tobi' } }); await tobi.destroy(); - let sql = dialect === 'db2' || dialect === 'oracle' ? 'SELECT * FROM "paranoidusers" WHERE "username"=\'Tobi\'' : 'SELECT * FROM paranoidusers WHERE username=\'Tobi\''; + let sql = ['db2', 'oracle'].includes(dialect) ? 'SELECT * FROM "paranoidusers" WHERE "username"=\'Tobi\'' : 'SELECT * FROM paranoidusers WHERE username=\'Tobi\''; let result = await this.sequelize.query(sql, { plain: true }); expect(result.username).to.equal('Tobi'); await User.destroy({ where: { username: 'Tony' } }); - sql = dialect === 'db2' || dialect === 'oracle' ? 'SELECT * FROM "paranoidusers" WHERE "username"=\'Tony\'' : 'SELECT * FROM paranoidusers WHERE username=\'Tony\''; + sql = ['db2', 'oracle'].includes(dialect) ? 'SELECT * FROM "paranoidusers" WHERE "username"=\'Tony\'' : 'SELECT * FROM paranoidusers WHERE username=\'Tony\''; result = await this.sequelize.query(sql, { plain: true }); expect(result.username).to.equal('Tony'); await User.destroy({ where: { username: ['Tony', 'Max'] }, force: true }); - sql = dialect === 'db2' || dialect === 'oracle' ? 'SELECT * FROM "paranoidusers"' : 'SELECT * FROM paranoidusers'; + sql = ['db2', 'oracle'].includes(dialect) ? 'SELECT * FROM "paranoidusers"' : 'SELECT * FROM paranoidusers'; const [users] = await this.sequelize.query(sql, { raw: true }); expect(users).to.have.length(1); expect(users[0].username).to.equal('Tobi'); @@ -1836,7 +1836,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { expect(count.find(i => i.data === 'B')).to.deep.equal({ data: 'B', count: 1 }); }); - if (dialect !== 'mssql' && dialect !== 'db2' && dialect !== 'oracle') { + if (!['mssql', 'db2', 'oracle'].includes(dialect)) { describe('aggregate', () => { it('allows grouping by aliased attribute', async function() { await this.User.aggregate('id', 'count', { diff --git a/test/integration/model/bulk-create.test.js b/test/integration/model/bulk-create.test.js index 633c1e8bd822..42720424818c 100644 --- a/test/integration/model/bulk-create.test.js +++ b/test/integration/model/bulk-create.test.js @@ -158,7 +158,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { style: 'ipa' }], { logging(sql) { - if (dialect === 'postgres' || dialect === 'oracle') { + if (['postgres', 'oracle'].includes(dialect)) { expect(sql).to.include('INSERT INTO "Beers" ("id","style","createdAt","updatedAt") VALUES (DEFAULT'); } else if (dialect === 'db2') { expect(sql).to.include('INSERT INTO "Beers" ("style","createdAt","updatedAt") VALUES'); diff --git a/test/integration/model/create.test.js b/test/integration/model/create.test.js index d559c9a445fa..3bae2752af9f 100644 --- a/test/integration/model/create.test.js +++ b/test/integration/model/create.test.js @@ -450,7 +450,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { }); } - (dialect !== 'sqlite' && dialect !== 'mssql' && dialect !== 'oracle' && dialect !== 'db2' ? it : it.skip)('should not fail silently with concurrency higher than pool, a unique constraint and a create hook resulting in mismatched values', async function() { + (!['sqlite', 'mssql', 'db2', 'oracle'].includes(dialect) ? it : it.skip)('should not fail silently with concurrency higher than pool, a unique constraint and a create hook resulting in mismatched values', async function() { const User = this.sequelize.define('user', { username: { type: DataTypes.STRING, diff --git a/test/integration/model/findAll/order.test.js b/test/integration/model/findAll/order.test.js index 894f0530938a..9775f8a0f428 100644 --- a/test/integration/model/findAll/order.test.js +++ b/test/integration/model/findAll/order.test.js @@ -23,7 +23,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { }); // Oracle doesn't support operators in Order by clause - if (current.dialect.name !== 'mssql' && current.dialect.name !== 'oracle') { + if (!['mssql', 'oracle'].includes(current.dialect.name)) { const email = current.dialect.name === 'db2' ? '"email"' : 'email'; it('should work with order: literal()', async function() { const users = await this.User.findAll({ @@ -87,7 +87,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { } it('should not throw on a literal', async function() { - if (current.dialect.name === 'db2' || current.dialect.name === 'oracle') { + if (['db2', 'oracle'].includes(current.dialect.name)) { await this.User.findAll({ order: [ ['id', this.sequelize.literal('ASC, "name" DESC')] diff --git a/test/integration/query-interface.test.js b/test/integration/query-interface.test.js index 126b7941ff82..1bb6ab319e98 100644 --- a/test/integration/query-interface.test.js +++ b/test/integration/query-interface.test.js @@ -66,7 +66,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { expect(tableNames).to.deep.equal(['my_test_table']); }); - if (dialect !== 'sqlite' && dialect !== 'postgres' && dialect !== 'db2' && dialect != 'oracle') { + if (!['sqlite', 'postgres', 'db2', 'oracle'].includes(dialect)) { // NOTE: sqlite doesn't allow querying between databases and // postgres requires creating a new connection to create a new table. it('should not show tables in other databases', async function() { diff --git a/test/integration/query-interface/changeColumn.test.js b/test/integration/query-interface/changeColumn.test.js index 69bd4f8df114..41ec3edd609f 100644 --- a/test/integration/query-interface/changeColumn.test.js +++ b/test/integration/query-interface/changeColumn.test.js @@ -227,7 +227,8 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { expect(describedTable.level_id.allowNull).to.not.equal(firstTable.level_id.allowNull); expect(describedTable.level_id.allowNull).to.be.equal(true); }); - if (dialect !== 'db2' && dialect !== 'oracle') { + // For Oracle, comments are not part of table description and are stored differently. + if (!['db2', 'oracle'].includes(dialect)) { it('should change the comment of column', async function() { const describedTable = await this.queryInterface.describeTable({ tableName: 'users' diff --git a/test/integration/sequelize/query.test.js b/test/integration/sequelize/query.test.js index 32a0039a4857..5822d935d82f 100644 --- a/test/integration/sequelize/query.test.js +++ b/test/integration/sequelize/query.test.js @@ -395,7 +395,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { const query = `INSERT INTO ${qq(this.User.tableName)} (username, email_address, ${ qq('createdAt') }, ${qq('updatedAt') }) VALUES ('duplicate', 'duplicate@gmail.com', '2012-01-01 10:10:10', '2012-01-01 10:10:10')`; - if (dialect === 'db2' || dialect === 'oracle') { + if (['db2', 'oracle'].includes(dialect)) { this.query = `INSERT INTO ${qq(this.User.tableName)} ("username", "email_address", ${ qq('createdAt') }, ${qq('updatedAt') }) VALUES ('duplicate', 'duplicate@gmail.com', '2012-01-01 10:10:10', '2012-01-01 10:10:10')`; @@ -409,7 +409,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { } catch (err) { error = err; } - if (dialect === 'db2' || dialect === 'oracle') { + if (['db2', 'oracle'].includes(dialect)) { expect(error).to.be.instanceOf(DatabaseError); } else { expect(error).to.be.instanceOf(UniqueConstraintError); @@ -426,7 +426,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { )}, ${qq( 'updatedAt' )}) VALUES (123456789, '2012-01-01 10:10:10', '2012-01-01 10:10:10', '2012-01-01 10:10:10')`; - if (dialect === 'db2' || dialect === 'oracle') { + if (['db2', 'oracle'].includes(dialect)) { this.query = `INSERT INTO ${qq(this.UserVisit.tableName)} ("user_id", "visited_at", ${qq( 'createdAt' )}, ${qq( @@ -437,7 +437,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { } catch (err) { error = err; } - if (dialect === 'db2' || dialect === 'oracle') { + if (['db2', 'oracle'].includes(dialect)) { expect(error).to.be.instanceOf(DatabaseError); } else { expect(error).to.be.instanceOf(ForeignKeyConstraintError); @@ -568,7 +568,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { date.setMilliseconds(0); let sql = 'select ? as number, ? as date,? as string,? as boolean,? as buffer'; - if (dialect === 'db2' || dialect === 'oracle') { + if (['db2', 'oracle'].includes(dialect)) { sql = `select ? as "number", ? as "date",? as "string",? as "boolean",? as "buffer"${ Support.addDualInSelect()}`; } const result = await this.sequelize.query({ @@ -608,7 +608,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { this.values = [1, 2]; } get query() { - if (dialect === 'db2' || dialect === 'oracle') { + if (['db2', 'oracle'].includes(dialect)) { return `select ? as "foo", ? as "bar"${ Support.addDualInSelect()}`; } return 'select ? as foo, ? as bar'; @@ -619,7 +619,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { expect(logSql).to.not.include('?'); }); - const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: 2 }] : [{ foo: 1, bar: 2 }]; + const expected = ['db2', 'oracle'].includes(dialect) ? [{ FOO: 1, BAR: 2 }] : [{ foo: 1, bar: 2 }]; it('uses properties `query` and `values` if query is tagged', async function() { let logSql; const result = await this.sequelize.query({ query: `select ? as foo, ? as bar${ Support.addDualInSelect()}`, values: [1, 2] }, { type: this.sequelize.QueryTypes.SELECT, logging(s) { logSql = s; } }); @@ -669,19 +669,19 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { }); it('replaces named parameters with the passed object and ignore those which does not qualify', async function() { - const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: 2, BAZ: '00:00' }] : [{ foo: 1, bar: 2, baz: '00:00' }]; + const expected = ['db2', 'oracle'].includes(dialect) ? [{ FOO: 1, BAR: 2, BAZ: '00:00' }] : [{ foo: 1, bar: 2, baz: '00:00' }]; await expect(this.sequelize.query(`select :one as foo, :two as bar, '00:00' as baz${ Support.addDualInSelect()}`, { raw: true, replacements: { one: 1, two: 2 } }).then(obj => obj[0])) .to.eventually.deep.equal(expected); }); it('replaces named parameters with the passed object using the same key twice', async function() { - const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: 2, BAZ: 1 }] : [{ foo: 1, bar: 2, baz: 1 }]; + const expected = ['db2', 'oracle'].includes(dialect) ? [{ FOO: 1, BAR: 2, BAZ: 1 }] : [{ foo: 1, bar: 2, baz: 1 }]; await expect(this.sequelize.query(`select :one as foo, :two as bar, :one as baz${ Support.addDualInSelect()}`, { raw: true, replacements: { one: 1, two: 2 } }).then(obj => obj[0])) .to.eventually.deep.equal(expected); }); it('replaces named parameters with the passed object having a null property', async function() { - const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: null }] : [{ foo: 1, bar: null }]; + const expected = ['db2', 'oracle'].includes(dialect) ? [{ FOO: 1, BAR: null }] : [{ foo: 1, bar: null }]; await expect(this.sequelize.query(`select :one as foo, :two as bar${ Support.addDualInSelect()}`, { raw: true, replacements: { one: 1, two: null } }).then(obj => obj[0])) .to.eventually.deep.equal(expected); }); @@ -726,7 +726,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { it('binds named parameters with the passed object having a null property', async function() { const typeCast = dialect === 'postgres' || dialect === 'db2' ? '::int' : ''; const result = await this.sequelize.query(`select $one${typeCast} as foo, $two${typeCast} as bar${Support.addDualInSelect()}`, { raw: true, bind: { one: 1, two: null } }); - const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: null }] : [{ foo: 1, bar: null }]; + const expected = ['db2', 'oracle'].includes(dialect) ? [{ FOO: 1, BAR: null }] : [{ foo: 1, bar: null }]; expect(result[0]).to.deep.equal(expected); }); @@ -734,7 +734,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { const typeCast = dialect === 'postgres' || dialect === 'db2' ? '::int' : ''; let logSql; const result = await this.sequelize.query(`select $1${typeCast} as foo, '$$ / $$1' as bar${Support.addDualInSelect()}`, { raw: true, bind: [1], logging(s) { logSql = s;} }); - const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: '$ / $1' }] : [{ foo: 1, bar: '$ / $1' }]; + const expected = ['db2', 'oracle'].includes(dialect) ? [{ FOO: 1, BAR: '$ / $1' }] : [{ foo: 1, bar: '$ / $1' }]; expect(result[0]).to.deep.equal(expected); if (['postgres', 'sqlite', 'db2'].includes(dialect)) { expect(logSql).to.include('$1'); @@ -744,14 +744,14 @@ describe(Support.getTestDialectTeaser('Sequelize'), () => { it('binds named parameters object handles escaped $$', async function() { const typeCast = dialect === 'postgres' || dialect === 'db2' ? '::int' : ''; const result = await this.sequelize.query(`select $one${typeCast} as foo, '$$ / $$one' as bar${Support.addDualInSelect()}`, { raw: true, bind: { one: 1 } }); - const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO: 1, BAR: '$ / $one' }] : [{ foo: 1, bar: '$ / $one' }]; + const expected = ['db2', 'oracle'].includes(dialect) ? [{ FOO: 1, BAR: '$ / $one' }] : [{ foo: 1, bar: '$ / $one' }]; expect(result[0]).to.deep.equal(expected); }); it('escape where has $ on the middle of characters', async function() { const typeCast = dialect === 'postgres' || dialect === 'db2' ? '::int' : ''; const result = await this.sequelize.query(`select $one${typeCast} as foo$bar${Support.addDualInSelect()}`, { raw: true, bind: { one: 1 } }); - const expected = dialect === 'db2' || dialect === 'oracle' ? [{ FOO$BAR: 1 }] : [{ foo$bar: 1 }]; + const expected = ['db2', 'oracle'].includes(dialect) ? [{ FOO$BAR: 1 }] : [{ foo$bar: 1 }]; expect(result[0]).to.deep.equal(expected); }); diff --git a/test/integration/utils.test.js b/test/integration/utils.test.js index d3f502d89234..c5cd85c6038f 100644 --- a/test/integration/utils.test.js +++ b/test/integration/utils.test.js @@ -159,8 +159,8 @@ describe(Support.getTestDialectTeaser('Utils'), () => { } ]); }); - - if (Support.getTestDialect() !== 'mssql' && Support.getTestDialect() !== 'oracle') { + + if (!['mssql', 'oracle'].includes(Support.getTestDialect())) { it('accepts condition object (with cast)', async function() { const type = Support.getTestDialect() === 'mysql' ? 'unsigned' : 'int'; @@ -188,7 +188,7 @@ describe(Support.getTestDialectTeaser('Utils'), () => { }); } - if (Support.getTestDialect() !== 'mssql' && Support.getTestDialect() !== 'postgres' && Support.getTestDialect() !== 'oracle') { + if (!['mssql', 'postgres', 'oracle'].includes(Support.getTestDialect())) { it('accepts condition object (auto casting)', async function() { const [airplane] = await Airplane.findAll({ attributes: [ diff --git a/test/unit/sql/generateJoin.test.js b/test/unit/sql/generateJoin.test.js index 6ce3f18e1d6c..29e6cbf28964 100644 --- a/test/unit/sql/generateJoin.test.js +++ b/test/unit/sql/generateJoin.test.js @@ -99,8 +99,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] }, { - oracle: 'LEFT OUTER JOIN "company" "Company" ON "User"."company_id" = "Company"."id"', - default: 'LEFT OUTER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id]' + default: 'LEFT OUTER JOIN [company] AS [Company] ON [User].[company_id] = [Company].[id]', + oracle: 'LEFT OUTER JOIN "company" "Company" ON "User"."company_id" = "Company"."id"' } ); @@ -154,8 +154,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] }, { - oracle: 'LEFT OUTER JOIN "company" "Company" ON "User"."companyId" = "Company"."id"', - default: 'LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]' + default: 'LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]', + oracle: 'LEFT OUTER JOIN "company" "Company" ON "User"."companyId" = "Company"."id"' } ); @@ -174,7 +174,6 @@ describe(Support.getTestDialectTeaser('SQL'), () => { default: "LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id] AND [Company].[name] = 'ABC'", oracle: 'LEFT OUTER JOIN "company" "Company" ON "User"."companyId" = "Company"."id" AND "Company"."name" = \'ABC\'', mssql: "LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id] AND [Company].[name] = N'ABC'" - } ); @@ -190,8 +189,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] }, { - oracle: 'RIGHT OUTER JOIN "company" "Company" ON "User"."companyId" = "Company"."id"', - default: `${current.dialect.supports['RIGHT JOIN'] ? 'RIGHT' : 'LEFT'} OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]` + default: `${current.dialect.supports['RIGHT JOIN'] ? 'RIGHT' : 'LEFT'} OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]`, + oracle: 'RIGHT OUTER JOIN "company" "Company" ON "User"."companyId" = "Company"."id"' } ); @@ -210,8 +209,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { - oracle: 'LEFT OUTER JOIN "user" "Company->Owner" ON "Company"."owner_id" = "Company->Owner"."id_user"', - default: 'LEFT OUTER JOIN [user] AS [Company->Owner] ON [Company].[owner_id] = [Company->Owner].[id_user]' + default: 'LEFT OUTER JOIN [user] AS [Company->Owner] ON [Company].[owner_id] = [Company->Owner].[id_user]', + oracle: 'LEFT OUTER JOIN "user" "Company->Owner" ON "Company"."owner_id" = "Company->Owner"."id_user"' } ); @@ -233,8 +232,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] }, { - oracle: 'LEFT OUTER JOIN "profession" "Company->Owner->Profession" ON "Company->Owner"."professionId" = "Company->Owner->Profession"."id"', - default: 'LEFT OUTER JOIN [profession] AS [Company->Owner->Profession] ON [Company->Owner].[professionId] = [Company->Owner->Profession].[id]' + default: 'LEFT OUTER JOIN [profession] AS [Company->Owner->Profession] ON [Company->Owner].[professionId] = [Company->Owner->Profession].[id]', + oracle: 'LEFT OUTER JOIN "profession" "Company->Owner->Profession" ON "Company->Owner"."professionId" = "Company->Owner->Profession"."id"' } ); @@ -253,9 +252,9 @@ describe(Support.getTestDialectTeaser('SQL'), () => { } ] }, - { - oracle: 'LEFT OUTER JOIN "user" "Company->Owner" ON "Company"."owner_id" = "Company->Owner"."id_user"', - default: 'LEFT OUTER JOIN [user] AS [Company->Owner] ON [Company].[owner_id] = [Company->Owner].[id_user]' + { + default: 'LEFT OUTER JOIN [user] AS [Company->Owner] ON [Company].[owner_id] = [Company->Owner].[id_user]', + oracle: 'LEFT OUTER JOIN "user" "Company->Owner" ON "Company"."owner_id" = "Company->Owner"."id_user"' } ); @@ -269,8 +268,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] }, { - oracle: 'INNER JOIN "company" "Company" ON "User"."companyId" = "Company"."id"', - default: 'INNER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]' + default: 'INNER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]', + oracle: 'INNER JOIN "company" "Company" ON "User"."companyId" = "Company"."id"' } ); @@ -286,9 +285,9 @@ describe(Support.getTestDialectTeaser('SQL'), () => { User.Tasks ] }, - { - oracle: 'LEFT OUTER JOIN "task" "Tasks" ON "User"."id_user" = "Tasks"."user_id"', - default: 'LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id_user] = [Tasks].[user_id]' + { + default: 'LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id_user] = [Tasks].[user_id]', + oracle: 'LEFT OUTER JOIN "task" "Tasks" ON "User"."id_user" = "Tasks"."user_id"' } ); @@ -303,8 +302,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { // The primary key of the main model will be aliased because it's coming from a subquery that the :M join is not a part of - oracle: 'LEFT OUTER JOIN "task" "Tasks" ON "User"."id" = "Tasks"."user_id"', - default: 'LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id] = [Tasks].[user_id]' + default: 'LEFT OUTER JOIN [task] AS [Tasks] ON [User].[id] = [Tasks].[user_id]', + oracle: 'LEFT OUTER JOIN "task" "Tasks" ON "User"."id" = "Tasks"."user_id"' } ); @@ -324,8 +323,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] }, { - oracle: 'LEFT OUTER JOIN "task" "Tasks" ON ("User"."id_user" = "Tasks"."user_id" OR "Tasks"."user_id" = 2)', - default: 'LEFT OUTER JOIN [task] AS [Tasks] ON ([User].[id_user] = [Tasks].[user_id] OR [Tasks].[user_id] = 2)' + default: 'LEFT OUTER JOIN [task] AS [Tasks] ON ([User].[id_user] = [Tasks].[user_id] OR [Tasks].[user_id] = 2)', + oracle: 'LEFT OUTER JOIN "task" "Tasks" ON ("User"."id_user" = "Tasks"."user_id" OR "Tasks"."user_id" = 2)' } ); @@ -340,9 +339,9 @@ describe(Support.getTestDialectTeaser('SQL'), () => { } ] }, - { - oracle: 'LEFT OUTER JOIN "task" "Tasks" ON "Tasks"."user_id" = "User"."alternative_id"', - default: 'LEFT OUTER JOIN [task] AS [Tasks] ON [Tasks].[user_id] = [User].[alternative_id]' + { + default: 'LEFT OUTER JOIN [task] AS [Tasks] ON [Tasks].[user_id] = [User].[alternative_id]', + oracle: 'LEFT OUTER JOIN "task" "Tasks" ON "Tasks"."user_id" = "User"."alternative_id"' } ); @@ -370,8 +369,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { - oracle: 'LEFT OUTER JOIN "user" "Company->Owner" ON ("Company"."owner_id" = "Company->Owner"."id_user" OR "Company->Owner"."id_user" = 2)', - default: 'LEFT OUTER JOIN [user] AS [Company->Owner] ON ([Company].[owner_id] = [Company->Owner].[id_user] OR [Company->Owner].[id_user] = 2)' + default: 'LEFT OUTER JOIN [user] AS [Company->Owner] ON ([Company].[owner_id] = [Company->Owner].[id_user] OR [Company->Owner].[id_user] = 2)', + oracle: 'LEFT OUTER JOIN "user" "Company->Owner" ON ("Company"."owner_id" = "Company->Owner"."id_user" OR "Company->Owner"."id_user" = 2)' } ); diff --git a/test/unit/sql/offset-limit.test.js b/test/unit/sql/offset-limit.test.js index edb705b37b53..04db33ab4441 100644 --- a/test/unit/sql/offset-limit.test.js +++ b/test/unit/sql/offset-limit.test.js @@ -100,7 +100,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { }, { db2: ' FETCH NEXT 10 ROWS ONLY', default: ' LIMIT 10', - oracle: ' OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', + oracle: ' ORDER BY "tableRef"."id" OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', mssql: ' ORDER BY [tableRef].[id] OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY' }); }); diff --git a/test/unit/sql/select.test.js b/test/unit/sql/select.test.js index 0fc49930ceed..1be3e85cfdaa 100644 --- a/test/unit/sql/select.test.js +++ b/test/unit/sql/select.test.js @@ -130,16 +130,16 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] } }, { - oracle: 'SELECT "user".* FROM (' + - 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 1 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + - 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 5 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub)' + - ' "user" ORDER BY "subquery_order_0" ASC;', default: `SELECT [user].* FROM (${ [ `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[last_name] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 1 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub`, `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[last_name] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 5 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub` ].join(current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ') - }) AS [user] ORDER BY [subquery_order_0] ASC;` + }) AS [user] ORDER BY [subquery_order_0] ASC;`, + oracle: 'SELECT "user".* FROM (' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 1 ORDER BY "subquery_order_0" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 5 ORDER BY "subquery_order_0" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub)' + + ' "user" ORDER BY "subquery_order_0" ASC;' }); testsql({ @@ -165,16 +165,16 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] } }, { - oracle: 'SELECT "user".* FROM (' + - 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 1 AND "project_users"."status" = 1 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + - 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 5 AND "project_users"."status" = 1 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub)' + - ' "user" ORDER BY "subquery_order_0" ASC;', default: `SELECT [user].* FROM (${ [ `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[last_name] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 1 AND [project_users].[status] = 1 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub`, `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[last_name] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 5 AND [project_users].[status] = 1 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub` ].join(current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ') - }) AS [user] ORDER BY [subquery_order_0] ASC;` + }) AS [user] ORDER BY [subquery_order_0] ASC;`, + oracle: 'SELECT "user".* FROM (' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 1 AND "project_users"."status" = 1 ORDER BY "subquery_order_0" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 5 AND "project_users"."status" = 1 ORDER BY "subquery_order_0" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub)' + + ' "user" ORDER BY "subquery_order_0" ASC;' }); testsql({ @@ -200,16 +200,16 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ] } }, { - oracle: 'SELECT "user".* FROM (' + - 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."id_user" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 1 WHERE "user"."age" >= 21 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + - 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."id_user" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 5 WHERE "user"."age" >= 21 ORDER BY "subquery_order_0" ASC, "user"."id_user" OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub) ' + - '"user" ORDER BY "subquery_order_0" ASC;', default: `SELECT [user].* FROM (${ [ `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[id_user] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 1 WHERE [user].[age] >= 21 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub`, `SELECT * FROM (SELECT [user].[id_user] AS [id], [user].[id_user] AS [subquery_order_0], [project_users].[user_id] AS [project_users.userId], [project_users].[project_id] AS [project_users.projectId] FROM [users] AS [user] INNER JOIN [project_users] AS [project_users] ON [user].[id_user] = [project_users].[user_id] AND [project_users].[project_id] = 5 WHERE [user].[age] >= 21 ORDER BY [subquery_order_0] ASC${ current.dialect.name === 'mssql' ? ', [user].[id_user]' : ''}${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub` ].join(current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ') - }) AS [user] ORDER BY [subquery_order_0] ASC;` + }) AS [user] ORDER BY [subquery_order_0] ASC;`, + oracle: 'SELECT "user".* FROM (' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."id_user" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 1 WHERE "user"."age" >= 21 ORDER BY "subquery_order_0" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + + 'SELECT * FROM (SELECT "user"."id_user" AS "id", "user"."id_user" AS "subquery_order_0", "project_users"."user_id" AS "project_users.userId", "project_users"."project_id" AS "project_users.projectId" FROM "users" "user" INNER JOIN "project_users" "project_users" ON "user"."id_user" = "project_users"."user_id" AND "project_users"."project_id" = 5 WHERE "user"."age" >= 21 ORDER BY "subquery_order_0" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub) ' + + '"user" ORDER BY "subquery_order_0" ASC;' }); }()); diff --git a/test/unit/sql/where.test.js b/test/unit/sql/where.test.js index bd79cc3f7dfc..4811b5d60f60 100644 --- a/test/unit/sql/where.test.js +++ b/test/unit/sql/where.test.js @@ -500,7 +500,6 @@ describe(Support.getTestDialectTeaser('SQL'), () => { [Op.between]: ['2013-01-01', '2013-01-11'] }, { default: "[date] BETWEEN '2013-01-01' AND '2013-01-11'", - oracle: '"date" BETWEEN \'2013-01-01\' AND \'2013-01-11\'', mssql: "[date] BETWEEN N'2013-01-01' AND N'2013-01-11'" }); From 0dba36862975509fc1904d7a37efe7a4062d5b00 Mon Sep 17 00:00:00 2001 From: Sudarshan <48428602+sudarshan12s@users.noreply.github.com> Date: Tue, 28 Jun 2022 09:32:05 +0530 Subject: [PATCH 04/14] fix: merge from sequelize-v6 --- src/dialects/abstract/query-generator.js | 6 +- src/dialects/postgres/query-generator.js | 8 +- src/model.js | 1 + .../dialects/postgres/query-generator.test.js | 109 ++++++++++++------ test/unit/sql/add-column.test.js | 85 ++++++++------ test/unit/sql/remove-column.test.js | 38 ++++-- 6 files changed, 164 insertions(+), 83 deletions(-) diff --git a/src/dialects/abstract/query-generator.js b/src/dialects/abstract/query-generator.js index 20bdadae41ab..02dd5e338743 100644 --- a/src/dialects/abstract/query-generator.js +++ b/src/dialects/abstract/query-generator.js @@ -43,7 +43,7 @@ class QueryGenerator { options = options || {}; tableName = tableName || {}; return { - schema: tableName.schema || options.schema || 'public', + schema: tableName.schema || options.schema || this.options.schema || 'public', tableName: _.isPlainObject(tableName) ? tableName.tableName : tableName, delimiter: tableName.delimiter || options.delimiter || '.' }; @@ -970,8 +970,8 @@ class QueryGenerator { /** * Split a list of identifiers by "." and quote each part. * - * @param {string} identifiers - * + * @param {string} identifiers + * * @returns {string} */ quoteIdentifiers(identifiers) { diff --git a/src/dialects/postgres/query-generator.js b/src/dialects/postgres/query-generator.js index 42afac47e3ce..3edecce6c6ee 100644 --- a/src/dialects/postgres/query-generator.js +++ b/src/dialects/postgres/query-generator.js @@ -124,7 +124,9 @@ class PostgresQueryGenerator extends AbstractQueryGenerator { } showTablesQuery() { - return "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type LIKE '%TABLE' AND table_name != 'spatial_ref_sys';"; + const schema = this.options.schema || 'public'; + + return `SELECT table_name FROM information_schema.tables WHERE table_schema = ${this.escape(schema)} AND table_type LIKE '%TABLE' AND table_name != 'spatial_ref_sys';`; } tableExistsQuery(tableName) { @@ -135,7 +137,7 @@ class PostgresQueryGenerator extends AbstractQueryGenerator { } describeTableQuery(tableName, schema) { - if (!schema) schema = 'public'; + schema = schema || this.options.schema || 'public'; return 'SELECT ' + 'pk.constraint_type as "Constraint",' + @@ -156,7 +158,7 @@ class PostgresQueryGenerator extends AbstractQueryGenerator { 'ON pk.table_schema=c.table_schema ' + 'AND pk.table_name=c.table_name ' + 'AND pk.column_name=c.column_name ' + - `WHERE c.table_name = ${this.escape(tableName)} AND c.table_schema = ${this.escape(schema)} `; + `WHERE c.table_name = ${this.escape(tableName)} AND c.table_schema = ${this.escape(schema)}`; } /** diff --git a/src/model.js b/src/model.js index 94426e006f28..dc5bbeb2c7c4 100644 --- a/src/model.js +++ b/src/model.js @@ -1734,6 +1734,7 @@ class Model { * @param {string} [options.searchPath=DEFAULT] An optional parameter to specify the schema search_path (Postgres only) * @param {boolean|Error} [options.rejectOnEmpty=false] Throws an error when no records found * @param {boolean} [options.dotNotation] Allows including tables having the same attribute/column names - which have a dot in them. + * @param {boolean} [options.nest=false] If true, transforms objects with `.` separated property names into nested objects. * * @see * {@link Sequelize#query} diff --git a/test/unit/dialects/postgres/query-generator.test.js b/test/unit/dialects/postgres/query-generator.test.js index 3ceb17d3961e..72dcee34baed 100644 --- a/test/unit/dialects/postgres/query-generator.test.js +++ b/test/unit/dialects/postgres/query-generator.test.js @@ -11,6 +11,10 @@ const chai = require('chai'), current = Support.sequelize, _ = require('lodash'); +const customSequelize = Support.createSequelizeInstance({ + schema: 'custom', +}); + if (dialect.startsWith('postgres')) { describe('[POSTGRES Specific] QueryGenerator', () => { const suites = { @@ -1205,49 +1209,49 @@ if (dialect.startsWith('postgres')) { { arguments: ['myTable', 'myColumn'], expectation: 'SELECT ' + - 'DISTINCT tc.constraint_name as constraint_name, ' + - 'tc.constraint_schema as constraint_schema, ' + - 'tc.constraint_catalog as constraint_catalog, ' + - 'tc.table_name as table_name,' + - 'tc.table_schema as table_schema,' + - 'tc.table_catalog as table_catalog,' + - 'tc.initially_deferred as initially_deferred,' + - 'tc.is_deferrable as is_deferrable,' + - 'kcu.column_name as column_name,' + - 'ccu.table_schema AS referenced_table_schema,' + - 'ccu.table_catalog AS referenced_table_catalog,' + - 'ccu.table_name AS referenced_table_name,' + - 'ccu.column_name AS referenced_column_name ' + + 'DISTINCT tc.constraint_name as constraint_name, ' + + 'tc.constraint_schema as constraint_schema, ' + + 'tc.constraint_catalog as constraint_catalog, ' + + 'tc.table_name as table_name,' + + 'tc.table_schema as table_schema,' + + 'tc.table_catalog as table_catalog,' + + 'tc.initially_deferred as initially_deferred,' + + 'tc.is_deferrable as is_deferrable,' + + 'kcu.column_name as column_name,' + + 'ccu.table_schema AS referenced_table_schema,' + + 'ccu.table_catalog AS referenced_table_catalog,' + + 'ccu.table_name AS referenced_table_name,' + + 'ccu.column_name AS referenced_column_name ' + 'FROM information_schema.table_constraints AS tc ' + - 'JOIN information_schema.key_column_usage AS kcu ' + - 'ON tc.constraint_name = kcu.constraint_name ' + - 'JOIN information_schema.constraint_column_usage AS ccu ' + - 'ON ccu.constraint_name = tc.constraint_name ' + + 'JOIN information_schema.key_column_usage AS kcu ' + + 'ON tc.constraint_name = kcu.constraint_name ' + + 'JOIN information_schema.constraint_column_usage AS ccu ' + + 'ON ccu.constraint_name = tc.constraint_name ' + 'WHERE constraint_type = \'FOREIGN KEY\' AND tc.table_name=\'myTable\' AND kcu.column_name = \'myColumn\'' }, { arguments: [{ schema: 'mySchema', tableName: 'myTable' }, 'myColumn'], expectation: 'SELECT ' + - 'DISTINCT tc.constraint_name as constraint_name, ' + - 'tc.constraint_schema as constraint_schema, ' + - 'tc.constraint_catalog as constraint_catalog, ' + - 'tc.table_name as table_name,' + - 'tc.table_schema as table_schema,' + - 'tc.table_catalog as table_catalog,' + - 'tc.initially_deferred as initially_deferred,' + - 'tc.is_deferrable as is_deferrable,' + - 'kcu.column_name as column_name,' + - 'ccu.table_schema AS referenced_table_schema,' + - 'ccu.table_catalog AS referenced_table_catalog,' + - 'ccu.table_name AS referenced_table_name,' + - 'ccu.column_name AS referenced_column_name ' + + 'DISTINCT tc.constraint_name as constraint_name, ' + + 'tc.constraint_schema as constraint_schema, ' + + 'tc.constraint_catalog as constraint_catalog, ' + + 'tc.table_name as table_name,' + + 'tc.table_schema as table_schema,' + + 'tc.table_catalog as table_catalog,' + + 'tc.initially_deferred as initially_deferred,' + + 'tc.is_deferrable as is_deferrable,' + + 'kcu.column_name as column_name,' + + 'ccu.table_schema AS referenced_table_schema,' + + 'ccu.table_catalog AS referenced_table_catalog,' + + 'ccu.table_name AS referenced_table_name,' + + 'ccu.column_name AS referenced_column_name ' + 'FROM information_schema.table_constraints AS tc ' + - 'JOIN information_schema.key_column_usage AS kcu ' + - 'ON tc.constraint_name = kcu.constraint_name ' + - 'JOIN information_schema.constraint_column_usage AS ccu ' + - 'ON ccu.constraint_name = tc.constraint_name ' + + 'JOIN information_schema.key_column_usage AS kcu ' + + 'ON tc.constraint_name = kcu.constraint_name ' + + 'JOIN information_schema.constraint_column_usage AS ccu ' + + 'ON ccu.constraint_name = tc.constraint_name ' + 'WHERE constraint_type = \'FOREIGN KEY\' AND tc.table_name=\'myTable\' AND kcu.column_name = \'myColumn\'' + - ' AND tc.table_schema = \'mySchema\'' + ' AND tc.table_schema = \'mySchema\'' } ] }; @@ -1319,5 +1323,40 @@ if (dialect.startsWith('postgres')) { }); }); }); + + describe('With custom schema in Sequelize options', () => { + beforeEach(function () { + this.queryGenerator = new QueryGenerator({ + sequelize: customSequelize, + _dialect: customSequelize.dialect, + }); + }); + + const customSchemaSuites = { + showTablesQuery: [ + { + title: 'showTablesQuery defaults to the schema set in Sequelize options', + arguments: [], + expectation: `SELECT table_name FROM information_schema.tables WHERE table_schema = 'custom' AND table_type LIKE '%TABLE' AND table_name != 'spatial_ref_sys';`, + }, + ], + describeTableQuery: [ + { + title: 'describeTableQuery defaults to the schema set in Sequelize options', + arguments: ['myTable', null], + expectation: `SELECT pk.constraint_type as "Constraint",c.column_name as "Field", c.column_default as "Default",c.is_nullable as "Null", (CASE WHEN c.udt_name = 'hstore' THEN c.udt_name ELSE c.data_type END) || (CASE WHEN c.character_maximum_length IS NOT NULL THEN '(' || c.character_maximum_length || ')' ELSE '' END) as "Type", (SELECT array_agg(e.enumlabel) FROM pg_catalog.pg_type t JOIN pg_catalog.pg_enum e ON t.oid=e.enumtypid WHERE t.typname=c.udt_name) AS "special", (SELECT pgd.description FROM pg_catalog.pg_statio_all_tables AS st INNER JOIN pg_catalog.pg_description pgd on (pgd.objoid=st.relid) WHERE c.ordinal_position=pgd.objsubid AND c.table_name=st.relname) AS "Comment" FROM information_schema.columns c LEFT JOIN (SELECT tc.table_schema, tc.table_name, cu.column_name, tc.constraint_type FROM information_schema.TABLE_CONSTRAINTS tc JOIN information_schema.KEY_COLUMN_USAGE cu ON tc.table_schema=cu.table_schema and tc.table_name=cu.table_name and tc.constraint_name=cu.constraint_name and tc.constraint_type='PRIMARY KEY') pk ON pk.table_schema=c.table_schema AND pk.table_name=c.table_name AND pk.column_name=c.column_name WHERE c.table_name = 'myTable' AND c.table_schema = 'custom'`, + }, + ], + }; + + _.each(customSchemaSuites, (customSchemaTests, customSchemaSuiteTitle) => { + for (const customSchemaTest of customSchemaTests) { + it(customSchemaTest.title, function () { + const convertedText = customSchemaTest.arguments ? this.queryGenerator[customSchemaSuiteTitle](...customSchemaTest.arguments) : this.queryGenerator[customSchemaSuiteTitle](); + expect(convertedText).to.equal(customSchemaTest.expectation); + }); + } + }); + }); }); } diff --git a/test/unit/sql/add-column.test.js b/test/unit/sql/add-column.test.js index a13f4d7f077d..acf5b3a2963c 100644 --- a/test/unit/sql/add-column.test.js +++ b/test/unit/sql/add-column.test.js @@ -1,68 +1,87 @@ 'use strict'; -const Support = require('../support'), - DataTypes = require('sequelize/lib/data-types'), - expectsql = Support.expectsql, - current = Support.sequelize, - sql = current.dialect.queryGenerator; +const Support = require('../support'); +const DataTypes = require('sequelize/lib/data-types'); +const expectsql = Support.expectsql; +const current = Support.sequelize; +const sql = current.dialect.queryGenerator; -if (['mysql', 'mariadb'].includes(current.dialect.name)) { - describe(Support.getTestDialectTeaser('SQL'), () => { - describe('addColumn', () => { +const customSequelize = Support.createSequelizeInstance({ + schema: 'custom', +}); +const customSql = customSequelize.dialect.queryGenerator; - const Model = current.define('users', { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - } - }, { timestamps: false }); +describe(Support.getTestDialectTeaser('SQL'), () => { + describe('addColumn', () => { + const User = current.define('User', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + }, { timestamps: false }); + if (['mysql', 'mariadb'].includes(current.dialect.name)) { it('properly generate alter queries', () => { - return expectsql(sql.addColumnQuery(Model.getTableName(), 'level_id', current.normalizeAttribute({ + return expectsql(sql.addColumnQuery(User.getTableName(), 'level_id', current.normalizeAttribute({ type: DataTypes.FLOAT, - allowNull: false + allowNull: false, })), { - mariadb: 'ALTER TABLE `users` ADD `level_id` FLOAT NOT NULL;', - mysql: 'ALTER TABLE `users` ADD `level_id` FLOAT NOT NULL;' + mariadb: 'ALTER TABLE `Users` ADD `level_id` FLOAT NOT NULL;', + mysql: 'ALTER TABLE `Users` ADD `level_id` FLOAT NOT NULL;', }); }); it('properly generate alter queries for foreign keys', () => { - return expectsql(sql.addColumnQuery(Model.getTableName(), 'level_id', current.normalizeAttribute({ + return expectsql(sql.addColumnQuery(User.getTableName(), 'level_id', current.normalizeAttribute({ type: DataTypes.INTEGER, references: { model: 'level', - key: 'id' + key: 'id', }, onUpdate: 'cascade', - onDelete: 'cascade' + onDelete: 'cascade', })), { - mariadb: 'ALTER TABLE `users` ADD `level_id` INTEGER, ADD CONSTRAINT `users_level_id_foreign_idx` FOREIGN KEY (`level_id`) REFERENCES `level` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;', - mysql: 'ALTER TABLE `users` ADD `level_id` INTEGER, ADD CONSTRAINT `users_level_id_foreign_idx` FOREIGN KEY (`level_id`) REFERENCES `level` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;' + mariadb: 'ALTER TABLE `Users` ADD `level_id` INTEGER, ADD CONSTRAINT `Users_level_id_foreign_idx` FOREIGN KEY (`level_id`) REFERENCES `level` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;', + mysql: 'ALTER TABLE `Users` ADD `level_id` INTEGER, ADD CONSTRAINT `Users_level_id_foreign_idx` FOREIGN KEY (`level_id`) REFERENCES `level` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;', }); }); it('properly generate alter queries with FIRST', () => { - return expectsql(sql.addColumnQuery(Model.getTableName(), 'test_added_col_first', current.normalizeAttribute({ + return expectsql(sql.addColumnQuery(User.getTableName(), 'test_added_col_first', current.normalizeAttribute({ type: DataTypes.STRING, - first: true + first: true, })), { - mariadb: 'ALTER TABLE `users` ADD `test_added_col_first` VARCHAR(255) FIRST;', - mysql: 'ALTER TABLE `users` ADD `test_added_col_first` VARCHAR(255) FIRST;' + mariadb: 'ALTER TABLE `Users` ADD `test_added_col_first` VARCHAR(255) FIRST;', + mysql: 'ALTER TABLE `Users` ADD `test_added_col_first` VARCHAR(255) FIRST;', }); }); it('properly generates alter queries with column level comment', () => { - return expectsql(sql.addColumnQuery(Model.getTableName(), 'column_with_comment', current.normalizeAttribute({ + return expectsql(sql.addColumnQuery(User.getTableName(), 'column_with_comment', current.normalizeAttribute({ type: DataTypes.STRING, - comment: 'This is a comment' + comment: 'This is a comment', })), { - mariadb: 'ALTER TABLE `users` ADD `column_with_comment` VARCHAR(255) COMMENT \'This is a comment\';', - mysql: 'ALTER TABLE `users` ADD `column_with_comment` VARCHAR(255) COMMENT \'This is a comment\';' + mariadb: 'ALTER TABLE `Users` ADD `column_with_comment` VARCHAR(255) COMMENT \'This is a comment\';', + mysql: 'ALTER TABLE `Users` ADD `column_with_comment` VARCHAR(255) COMMENT \'This is a comment\';', }); }); + } + + it('defaults the schema to the one set in the Sequelize options', () => { + return expectsql(customSql.addColumnQuery(User.getTableName(), 'level_id', customSequelize.normalizeAttribute({ + type: DataTypes.FLOAT, + allowNull: false, + })), { + mariadb: 'ALTER TABLE `Users` ADD `level_id` FLOAT NOT NULL;', + mysql: 'ALTER TABLE `Users` ADD `level_id` FLOAT NOT NULL;', + postgres: 'ALTER TABLE "custom"."Users" ADD COLUMN "level_id" FLOAT NOT NULL;', + sqlite: 'ALTER TABLE `Users` ADD `level_id` FLOAT NOT NULL;', + mssql: 'ALTER TABLE [Users] ADD [level_id] FLOAT NOT NULL;', + db2: 'ALTER TABLE "Users" ADD "level_id" FLOAT NOT NULL;', + snowflake: 'ALTER TABLE "Users" ADD "level_id" FLOAT NOT NULL;', + }); }); }); -} +}); diff --git a/test/unit/sql/remove-column.test.js b/test/unit/sql/remove-column.test.js index e825d4766c11..c1b1c8bf6c01 100644 --- a/test/unit/sql/remove-column.test.js +++ b/test/unit/sql/remove-column.test.js @@ -1,19 +1,25 @@ 'use strict'; -const Support = require('../support'), - expectsql = Support.expectsql, - current = Support.sequelize, - sql = current.dialect.queryGenerator; +const Support = require('../support'); + +const expectsql = Support.expectsql; +const current = Support.sequelize; +const sql = current.dialect.queryGenerator; + +const customSequelize = Support.createSequelizeInstance({ + schema: 'custom', +}); +const customSql = customSequelize.dialect.queryGenerator; // Notice: [] will be replaced by dialect specific tick/quote character when there is not dialect specific expectation but only a default expectation -if (current.dialect.name !== 'sqlite') { - describe(Support.getTestDialectTeaser('SQL'), () => { - describe('removeColumn', () => { +describe(Support.getTestDialectTeaser('SQL'), () => { + describe('removeColumn', () => { + if (current.dialect.name !== 'sqlite') { it('schema', () => { expectsql(sql.removeColumnQuery({ schema: 'archive', - tableName: 'user' + tableName: 'user', }, 'email'), { mssql: 'ALTER TABLE [archive].[user] DROP COLUMN [email];', db2: 'ALTER TABLE "archive"."user" DROP COLUMN "email";', @@ -24,6 +30,20 @@ if (current.dialect.name !== 'sqlite') { snowflake: 'ALTER TABLE "archive"."user" DROP "email";' }); }); + } + + it('defaults the schema to the one set in the Sequelize options', () => { + expectsql(customSql.removeColumnQuery({ + tableName: 'user', + }, 'email'), { + mssql: 'ALTER TABLE [user] DROP COLUMN [email];', + db2: 'ALTER TABLE "user" DROP COLUMN "email";', + mariadb: 'ALTER TABLE `user` DROP `email`;', + mysql: 'ALTER TABLE `user` DROP `email`;', + postgres: 'ALTER TABLE "custom"."user" DROP COLUMN "email";', + snowflake: 'ALTER TABLE "user" DROP "email";', + sqlite: 'CREATE TABLE IF NOT EXISTS `user_backup` (`0` e, `1` m, `2` a, `3` i, `4` l);INSERT INTO `user_backup` SELECT `0`, `1`, `2`, `3`, `4` FROM `user`;DROP TABLE `user`;CREATE TABLE IF NOT EXISTS `user` (`0` e, `1` m, `2` a, `3` i, `4` l);INSERT INTO `user` SELECT `0`, `1`, `2`, `3`, `4` FROM `user_backup`;DROP TABLE `user_backup`;', + }); }); }); -} +}); From 1ae66738e7e73997479049449c74eafa2cb46107 Mon Sep 17 00:00:00 2001 From: Sudarshan <48428602+sudarshan12s@users.noreply.github.com> Date: Tue, 28 Jun 2022 10:49:51 +0530 Subject: [PATCH 05/14] fix: enable newly added unit tests for Oracle dialect --- test/unit/sql/add-column.test.js | 1 + test/unit/sql/remove-column.test.js | 1 + 2 files changed, 2 insertions(+) diff --git a/test/unit/sql/add-column.test.js b/test/unit/sql/add-column.test.js index acf5b3a2963c..7f0abfd88319 100644 --- a/test/unit/sql/add-column.test.js +++ b/test/unit/sql/add-column.test.js @@ -81,6 +81,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mssql: 'ALTER TABLE [Users] ADD [level_id] FLOAT NOT NULL;', db2: 'ALTER TABLE "Users" ADD "level_id" FLOAT NOT NULL;', snowflake: 'ALTER TABLE "Users" ADD "level_id" FLOAT NOT NULL;', + oracle: 'ALTER TABLE "Users" ADD "level_id" BINARY_FLOAT NOT NULL' }); }); }); diff --git a/test/unit/sql/remove-column.test.js b/test/unit/sql/remove-column.test.js index c1b1c8bf6c01..7e361fb52623 100644 --- a/test/unit/sql/remove-column.test.js +++ b/test/unit/sql/remove-column.test.js @@ -43,6 +43,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'ALTER TABLE "custom"."user" DROP COLUMN "email";', snowflake: 'ALTER TABLE "user" DROP "email";', sqlite: 'CREATE TABLE IF NOT EXISTS `user_backup` (`0` e, `1` m, `2` a, `3` i, `4` l);INSERT INTO `user_backup` SELECT `0`, `1`, `2`, `3`, `4` FROM `user`;DROP TABLE `user`;CREATE TABLE IF NOT EXISTS `user` (`0` e, `1` m, `2` a, `3` i, `4` l);INSERT INTO `user` SELECT `0`, `1`, `2`, `3`, `4` FROM `user_backup`;DROP TABLE `user_backup`;', + oracle: 'ALTER TABLE "user" DROP COLUMN "email";' }); }); }); From 478728d5077596474f5a177935d14aa8022401b4 Mon Sep 17 00:00:00 2001 From: Sudarshan Soma <48428602+sudarshan12s@users.noreply.github.com> Date: Tue, 28 Jun 2022 12:03:18 +0530 Subject: [PATCH 06/14] fix: remove dangling comma (#13) * fix: doc gen is fixed --- src/dialects/oracle/query-generator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialects/oracle/query-generator.js b/src/dialects/oracle/query-generator.js index a020a8510b4b..ea701a57c497 100644 --- a/src/dialects/oracle/query-generator.js +++ b/src/dialects/oracle/query-generator.js @@ -1267,7 +1267,7 @@ export class OracleQueryGenerator extends AbstractQueryGenerator { if (options.limit || options.offset) { // Add needed order by clause only when it is not provided - if (!orders.mainQueryOrder?.length || isSubQuery && !orders.subQueryOrder?.length) { + if (!orders.mainQueryOrder || !orders.mainQueryOrder.length || isSubQuery && (!orders.subQueryOrder || !orders.subQueryOrder.length)) { const tablePkFragment = `${this.quoteTable(options.tableAs || model.name)}.${this.quoteIdentifier(model.primaryKeyField)}`; fragment += ` ORDER BY ${tablePkFragment}`; } From a9c456e92c9d5b13ce457e7aab4e4a99356b9957 Mon Sep 17 00:00:00 2001 From: Sudarshan Soma <48428602+sudarshan12s@users.noreply.github.com> Date: Thu, 30 Jun 2022 14:13:38 +0530 Subject: [PATCH 07/14] fix: autogenerate the primary constraint name (#14) * fix: autogenerate the primary constraint name --- src/dialects/oracle/query-generator.js | 84 +++---------------- src/dialects/oracle/query.js | 8 +- test/integration/utils.test.js | 1 - test/unit/query-interface/bulk-insert.test.js | 1 + test/unit/sql/create-table.test.js | 6 +- 5 files changed, 20 insertions(+), 80 deletions(-) diff --git a/src/dialects/oracle/query-generator.js b/src/dialects/oracle/query-generator.js index ea701a57c497..828725123004 100644 --- a/src/dialects/oracle/query-generator.js +++ b/src/dialects/oracle/query-generator.js @@ -168,27 +168,7 @@ export class OracleQueryGenerator extends AbstractQueryGenerator { const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', '); if (pkString.length > 0) { - // PrimarykeyName would be of form "PK_table_col" - // Since values.table and pkstring has quoted values we first replace "" with _ - // then we replace [,"\s] with '' - // If primary key name exceeds 128 then we let Oracle DB autogenerate the constraint name - let primaryKeyName = `PK_${values.table}_${pkString}`.replace(/""/g, '_').replace(/[,"\s]/g, ''); - - if (primaryKeyName.length > 128) { - primaryKeyName = `PK_${values.table}`.replace(/""/g, '_').replace(/[,"\s]/g, ''); - } - - if (primaryKeyName.length > 128) { - primaryKeyName = ''; - } - - if (primaryKeyName.length > 0) { - values.attributes += - `,CONSTRAINT ${this.quoteIdentifier(primaryKeyName)} PRIMARY KEY (${pkString})`; - } else { - values.attributes += `,PRIMARY KEY (${pkString})`; - } - + values.attributes += `,PRIMARY KEY (${pkString})`; } // Dealing with FKs @@ -244,11 +224,7 @@ export class OracleQueryGenerator extends AbstractQueryGenerator { } if (canContinue) { - let indexName = 'name' in index ? index.name : ''; - - if (indexName === '') { - indexName = this._generateUniqueConstraintName(values.table, fields); - } + const indexName = 'name' in index ? index.name : ''; const constraintToAdd = { name: indexName, fields @@ -311,20 +287,14 @@ export class OracleQueryGenerator extends AbstractQueryGenerator { // Oracle cannot have an unique AND a primary key on the same fields, prior to the primary key if (canBeUniq) { - if (!_.isString(indexName)) { - indexName = this._generateUniqueConstraintName(values.table, columns.fields); - } - const index = options.uniqueKeys[columns.name]; delete options.uniqueKeys[columns.name]; indexName = indexName.replace(/[.,\s]/g, ''); columns.name = indexName; options.uniqueKeys[indexName] = index; - // We cannot auto-generate unique constraint name because sequelize tries to - // Add unique column again when it doesn't find unique constraint name after doing showIndexQuery - // MYSQL doesn't support constraint name > 64 and they face similar issue if size exceed 64 chars - if (indexName.length > 128) { + // Autogenerate Constraint name, if no indexName is given + if (indexName.length === 0) { values.attributes += `,UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ') })`; } else { values.attributes += @@ -357,33 +327,22 @@ export class OracleQueryGenerator extends AbstractQueryGenerator { const [tableName, schemaName] = this.getSchemaNameAndTableName(table); return `SELECT TABLE_NAME FROM ALL_TABLES WHERE TABLE_NAME = ${this.escape(tableName)} AND OWNER = ${table.schema ? this.escape(schemaName) : 'USER'}`; } - - /** - * Generates a name for an unique constraint with the pattern : uniqTABLENAMECOLUMNNAMES - * If this indexName is too long for Oracle, it's hashed to have an acceptable length - * - * @param {object|string} table - * @param {Array} columns - */ - _generateUniqueConstraintName(table, columns) { - const indexName = `uniq${table}${columns.join('')}`.replace(/[.,"\s]/g, '').toLowerCase(); - return indexName; - } describeTableQuery(tableName, schema) { const currTableName = this.getCatalogName(tableName.tableName || tableName); schema = this.getCatalogName(schema); // name, type, datalength (except number / nvarchar), datalength varchar, datalength number, nullable, default value, primary ? return [ - 'SELECT atc.COLUMN_NAME, atc.DATA_TYPE, atc.DATA_LENGTH, atc.CHAR_LENGTH, atc.DEFAULT_LENGTH, atc.NULLABLE, ', - "CASE WHEN ucc.CONSTRAINT_NAME LIKE'%PK%' THEN 'PRIMARY' ELSE '' END AS \"PRIMARY\" ", + 'SELECT atc.COLUMN_NAME, atc.DATA_TYPE, atc.DATA_LENGTH, atc.CHAR_LENGTH, atc.DEFAULT_LENGTH, atc.NULLABLE, ucc.constraint_type ', 'FROM all_tab_columns atc ', - 'LEFT OUTER JOIN all_cons_columns ucc ON(atc.table_name = ucc.table_name AND atc.COLUMN_NAME = ucc.COLUMN_NAME ) ', + 'LEFT OUTER JOIN ', + '(SELECT acc.column_name, acc.table_name, ac.constraint_type FROM all_cons_columns acc INNER JOIN all_constraints ac ON acc.constraint_name = ac.constraint_name) ucc ', + 'ON (atc.table_name = ucc.table_name AND atc.COLUMN_NAME = ucc.COLUMN_NAME) ', schema ? `WHERE (atc.OWNER = ${this.escape(schema)}) ` : 'WHERE atc.OWNER = USER ', `AND (atc.TABLE_NAME = ${this.escape(currTableName)})`, - 'ORDER BY "PRIMARY", atc.COLUMN_NAME' + 'ORDER BY atc.COLUMN_NAME' ].join(''); } @@ -845,10 +804,12 @@ export class OracleQueryGenerator extends AbstractQueryGenerator { showIndexesQuery(table) { const [tableName, owner] = this.getSchemaNameAndTableName(table); const sql = [ - 'SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, i.descend ', + 'SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, i.descend, c.constraint_type ', 'FROM all_ind_columns i ', 'INNER JOIN all_indexes u ', 'ON (u.table_name = i.table_name AND u.index_name = i.index_name) ', + 'LEFT OUTER JOIN all_constraints c ', + 'ON (c.table_name = i.table_name AND c.index_name = i.index_name) ', `WHERE i.table_name = ${this.escape(tableName)}`, ' AND u.table_owner = ', owner ? this.escape(owner) : 'USER', @@ -1050,27 +1011,6 @@ export class OracleQueryGenerator extends AbstractQueryGenerator { return sql; } - nameIndexes(indexes, rawTablename) { - let tableName; - if (_.isObject(rawTablename)) { - tableName = `${rawTablename.schema}.${rawTablename.tableName}`; - } else { - tableName = rawTablename; - } - return _.map(indexes, index => { - if (Object.prototype.hasOwnProperty.call(index, 'name')) return; - if (index.unique) { - index.name = this._generateUniqueConstraintName(tableName, index.fields); - } else { - const onlyAttributeNames = index.fields.map(field => - typeof field === 'string' ? field : field.name || field.attribute - ); - index.name = Utils.underscore(`${tableName}_${onlyAttributeNames.join('_')}`); - } - return index; - }); - } - dropForeignKeyQuery(tableName, foreignKey) { return this.dropConstraintQuery(tableName, foreignKey); } diff --git a/src/dialects/oracle/query.js b/src/dialects/oracle/query.js index b803c632a311..6380d3c303a0 100644 --- a/src/dialects/oracle/query.js +++ b/src/dialects/oracle/query.js @@ -419,7 +419,7 @@ export class OracleQuery extends AbstractQuery { type: _result.DATA_TYPE.toUpperCase(), allowNull: _result.NULLABLE === 'N' ? false : true, defaultValue: undefined, - primaryKey: _result.PRIMARY === 'PRIMARY' + primaryKey: _result.CONSTRAINT_TYPE === 'P' }; } }); @@ -580,7 +580,7 @@ export class OracleQuery extends AbstractQuery { if (!acc[indexRecord.INDEX_NAME]) { acc[indexRecord.INDEX_NAME] = { unique: indexRecord.UNIQUENESS === 'UNIQUE' ? true : false, - primary: indexRecord.INDEX_NAME.toLowerCase().indexOf('pk') === 0, + primary: indexRecord.CONSTRAINT_TYPE === 'P', name: indexRecord.INDEX_NAME.toLowerCase(), tableName: indexRecord.TABLE_NAME.toLowerCase(), type: undefined @@ -603,8 +603,8 @@ export class OracleQuery extends AbstractQuery { const columns = {}; columns.fields = acc[accKey].fields; // We are generating index field name in the format sequelize expects - // to avoid creating a unique index on primary key column - if (acc[accKey].primary === true) { + // to avoid creating a unique index on auto-generated index name + if (acc[accKey].name.match(/sys_c[0-9]*/)) { acc[accKey].name = Utils.nameIndex(columns, acc[accKey].tableName).name; } returnIndexes.push(acc[accKey]); diff --git a/test/integration/utils.test.js b/test/integration/utils.test.js index c5cd85c6038f..ea3e09bbbd4c 100644 --- a/test/integration/utils.test.js +++ b/test/integration/utils.test.js @@ -159,7 +159,6 @@ describe(Support.getTestDialectTeaser('Utils'), () => { } ]); }); - if (!['mssql', 'oracle'].includes(Support.getTestDialect())) { it('accepts condition object (with cast)', async function() { const type = Support.getTestDialect() === 'mysql' ? 'unsigned' : 'int'; diff --git a/test/unit/query-interface/bulk-insert.test.js b/test/unit/query-interface/bulk-insert.test.js index 1243ea063de6..b9a77b98df28 100644 --- a/test/unit/query-interface/bulk-insert.test.js +++ b/test/unit/query-interface/bulk-insert.test.js @@ -14,6 +14,7 @@ describe('QueryInterface#bulkInsert', () => { }); // you'll find more replacement tests in query-generator tests + // The Oracle dialect doesn't support replacements for bulkInsert (dialect !== 'oracle' ? it : it.skip)('does not parse replacements outside of raw sql', async () => { const getSql = stubQueryRun(); diff --git a/test/unit/sql/create-table.test.js b/test/unit/sql/create-table.test.js index 0cdf47ff0974..de2293060c50 100644 --- a/test/unit/sql/create-table.test.js +++ b/test/unit/sql/create-table.test.js @@ -27,7 +27,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'CREATE TABLE IF NOT EXISTS "foo"."users" ("id" SERIAL , "mood" "foo"."enum_users_mood", PRIMARY KEY ("id"));', mariadb: "CREATE TABLE IF NOT EXISTS `foo`.`users` (`id` INTEGER NOT NULL auto_increment , `mood` ENUM('happy', 'sad'), PRIMARY KEY (`id`)) ENGINE=InnoDB;", mysql: "CREATE TABLE IF NOT EXISTS `foo.users` (`id` INTEGER NOT NULL auto_increment , `mood` ENUM('happy', 'sad'), PRIMARY KEY (`id`)) ENGINE=InnoDB;", - oracle: 'BEGIN EXECUTE IMMEDIATE \'CREATE TABLE "foo"."users" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY, "mood" VARCHAR2(512) CHECK ("mood" IN(\'\'happy\'\', \'\'sad\'\')),CONSTRAINT "PK_foo.users_id" PRIMARY KEY ("id"))\'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;', + oracle: 'BEGIN EXECUTE IMMEDIATE \'CREATE TABLE "foo"."users" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY, "mood" VARCHAR2(512) CHECK ("mood" IN(\'\'happy\'\', \'\'sad\'\')),PRIMARY KEY ("id"))\'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;', mssql: "IF OBJECT_ID('[foo].[users]', 'U') IS NULL CREATE TABLE [foo].[users] ([id] INTEGER NOT NULL IDENTITY(1,1) , [mood] VARCHAR(255) CHECK ([mood] IN(N'happy', N'sad')), PRIMARY KEY ([id]));" }); }); @@ -53,7 +53,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { it('references right schema when adding foreign key #9029', () => { expectsql(sql.createTableQuery(BarProject.getTableName(), sql.attributesToSQL(BarProject.rawAttributes), { }), { - oracle: 'BEGIN EXECUTE IMMEDIATE \'CREATE TABLE "bar"."projects" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY, "user_id" INTEGER NULL,CONSTRAINT "PK_bar.projects_id" PRIMARY KEY ("id"),FOREIGN KEY ("user_id") REFERENCES "bar"."users" ("id") )\'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;', + oracle: 'BEGIN EXECUTE IMMEDIATE \'CREATE TABLE "bar"."projects" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY, "user_id" INTEGER NULL,PRIMARY KEY ("id"),FOREIGN KEY ("user_id") REFERENCES "bar"."users" ("id") )\'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;', sqlite: 'CREATE TABLE IF NOT EXISTS `bar.projects` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `user_id` INTEGER REFERENCES `bar.users` (`id`) ON DELETE NO ACTION ON UPDATE CASCADE);', db2: 'CREATE TABLE "bar"."projects" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , "user_id" INTEGER, PRIMARY KEY ("id"), FOREIGN KEY ("user_id") REFERENCES "bar"."users" ("id") ON DELETE NO ACTION);', postgres: 'CREATE TABLE IF NOT EXISTS "bar"."projects" ("id" SERIAL , "user_id" INTEGER REFERENCES "bar"."users" ("id") ON DELETE NO ACTION ON UPDATE CASCADE, PRIMARY KEY ("id"));', @@ -87,7 +87,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'CREATE TABLE "images" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY(START WITH 1, INCREMENT BY 1) , PRIMARY KEY ("id"), FOREIGN KEY ("id") REFERENCES "files" ("id"));', mariadb: 'CREATE TABLE IF NOT EXISTS `images` (`id` INTEGER auto_increment , PRIMARY KEY (`id`), FOREIGN KEY (`id`) REFERENCES `files` (`id`)) ENGINE=InnoDB;', mysql: 'CREATE TABLE IF NOT EXISTS `images` (`id` INTEGER auto_increment , PRIMARY KEY (`id`), FOREIGN KEY (`id`) REFERENCES `files` (`id`)) ENGINE=InnoDB;', - oracle: 'BEGIN EXECUTE IMMEDIATE \'CREATE TABLE "images" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY ,CONSTRAINT "PK_images_id" PRIMARY KEY ("id"),FOREIGN KEY ("id") REFERENCES "files" ("id"))\'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;', + oracle: 'BEGIN EXECUTE IMMEDIATE \'CREATE TABLE "images" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY ,PRIMARY KEY ("id"),FOREIGN KEY ("id") REFERENCES "files" ("id"))\'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;', mssql: 'IF OBJECT_ID(\'[images]\', \'U\') IS NULL CREATE TABLE [images] ([id] INTEGER IDENTITY(1,1) , PRIMARY KEY ([id]), FOREIGN KEY ([id]) REFERENCES [files] ([id]));' }); }); From 4cc8acea18ee988b38e22d8c9c03184154f83085 Mon Sep 17 00:00:00 2001 From: Sudarshan <48428602+sudarshan12s@users.noreply.github.com> Date: Mon, 18 Jul 2022 21:36:25 +0530 Subject: [PATCH 08/14] fix: remove trailing comma --- test/unit/sql/remove-column.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/sql/remove-column.test.js b/test/unit/sql/remove-column.test.js index d687ea858108..eb450d4f393b 100644 --- a/test/unit/sql/remove-column.test.js +++ b/test/unit/sql/remove-column.test.js @@ -27,7 +27,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mysql: 'ALTER TABLE `archive.user` DROP `email`;', postgres: 'ALTER TABLE "archive"."user" DROP COLUMN "email";', snowflake: 'ALTER TABLE "archive"."user" DROP "email";', - oracle: 'ALTER TABLE "archive"."user" DROP COLUMN "email";', + oracle: 'ALTER TABLE "archive"."user" DROP COLUMN "email";' }); }); } @@ -43,7 +43,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'ALTER TABLE "custom"."user" DROP COLUMN "email";', snowflake: 'ALTER TABLE "user" DROP "email";', sqlite: 'CREATE TABLE IF NOT EXISTS `user_backup` (`0` e, `1` m, `2` a, `3` i, `4` l);INSERT INTO `user_backup` SELECT `0`, `1`, `2`, `3`, `4` FROM `user`;DROP TABLE `user`;CREATE TABLE IF NOT EXISTS `user` (`0` e, `1` m, `2` a, `3` i, `4` l);INSERT INTO `user` SELECT `0`, `1`, `2`, `3`, `4` FROM `user_backup`;DROP TABLE `user_backup`;', - oracle: 'ALTER TABLE "user" DROP COLUMN "email";', + oracle: 'ALTER TABLE "user" DROP COLUMN "email";' }); }); }); From 336ef8e705f8a85b404fa73a94fac950cf72d41d Mon Sep 17 00:00:00 2001 From: Sudarshan <48428602+sudarshan12s@users.noreply.github.com> Date: Mon, 29 Aug 2022 12:13:32 +0530 Subject: [PATCH 09/14] fix: make changes to ORADERBY as per v6 sync --- test/unit/sql/select.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/unit/sql/select.test.js b/test/unit/sql/select.test.js index 5058bc5d4624..95b691212765 100644 --- a/test/unit/sql/select.test.js +++ b/test/unit/sql/select.test.js @@ -292,9 +292,9 @@ describe(Support.getTestDialectTeaser('SQL'), () => { } }, { oracle: 'SELECT "user".*, "POSTS"."id" AS "POSTS.id", "POSTS"."title" AS "POSTS.title" FROM (' + - 'SELECT * FROM (SELECT "id_user" AS "id", "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "users" "user" WHERE "user"."companyId" = 1 ORDER BY "user"."last_name" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + - 'SELECT * FROM (SELECT "id_user" AS "id", "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "users" "user" WHERE "user"."companyId" = 5 ORDER BY "user"."last_name" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub) ' + - '"user" LEFT OUTER JOIN "post" "POSTS" ON "user"."id" = "POSTS"."user_id" ORDER BY "user"."last_name" ASC;', + 'SELECT * FROM (SELECT "id_user" AS "id", "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "users" "user" WHERE "user"."companyId" = 1 ORDER BY "lastName" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + + 'SELECT * FROM (SELECT "id_user" AS "id", "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "users" "user" WHERE "user"."companyId" = 5 ORDER BY "lastName" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub) ' + + '"user" LEFT OUTER JOIN "post" "POSTS" ON "user"."id" = "POSTS"."user_id" ORDER BY "lastName" ASC;', default: `SELECT [user].*, [POSTS].[id] AS [POSTS.id], [POSTS].[title] AS [POSTS.title] FROM (${ [ `SELECT * FROM (SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] AS [user] WHERE [user].[companyId] = 1 ORDER BY [lastName] ASC${sql.addLimitAndOffset({ limit: 3, order: [['last_name', 'ASC']] })}) AS sub`, @@ -390,9 +390,9 @@ describe(Support.getTestDialectTeaser('SQL'), () => { } }, { oracle: 'SELECT "user".*, "POSTS"."id" AS "POSTS.id", "POSTS"."title" AS "POSTS.title", "POSTS->COMMENTS"."id" AS "POSTS.COMMENTS.id", "POSTS->COMMENTS"."title" AS "POSTS.COMMENTS.title" FROM (' + - 'SELECT * FROM (SELECT "id_user" AS "id", "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "users" "user" WHERE "user"."companyId" = 1 ORDER BY "user"."last_name" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + - 'SELECT * FROM (SELECT "id_user" AS "id", "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "users" "user" WHERE "user"."companyId" = 5 ORDER BY "user"."last_name" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub)' + - ' "user" LEFT OUTER JOIN "post" "POSTS" ON "user"."id" = "POSTS"."user_id" LEFT OUTER JOIN "comment" "POSTS->COMMENTS" ON "POSTS"."id" = "POSTS->COMMENTS"."post_id" ORDER BY "user"."last_name" ASC;', + 'SELECT * FROM (SELECT "id_user" AS "id", "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "users" "user" WHERE "user"."companyId" = 1 ORDER BY "lastName" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub UNION ALL ' + + 'SELECT * FROM (SELECT "id_user" AS "id", "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "users" "user" WHERE "user"."companyId" = 5 ORDER BY "lastName" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub)' + + ' "user" LEFT OUTER JOIN "post" "POSTS" ON "user"."id" = "POSTS"."user_id" LEFT OUTER JOIN "comment" "POSTS->COMMENTS" ON "POSTS"."id" = "POSTS->COMMENTS"."post_id" ORDER BY "lastName" ASC;', default: `SELECT [user].*, [POSTS].[id] AS [POSTS.id], [POSTS].[title] AS [POSTS.title], [POSTS->COMMENTS].[id] AS [POSTS.COMMENTS.id], [POSTS->COMMENTS].[title] AS [POSTS.COMMENTS.title] FROM (${ [ `SELECT * FROM (SELECT [id_user] AS [id], [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [users] AS [user] WHERE [user].[companyId] = 1 ORDER BY [lastName] ASC${sql.addLimitAndOffset({ limit: 3, order: ['last_name', 'ASC'] })}) AS sub`, From 9c0de49d915bbfe399f8af0c1f0029ebc76883fc Mon Sep 17 00:00:00 2001 From: Sudarshan <48428602+sudarshan12s@users.noreply.github.com> Date: Thu, 8 Sep 2022 12:43:59 +0530 Subject: [PATCH 10/14] fix: move test-unit-oracle above test-unit-all --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cfd67893f238..2ffbb652852d 100644 --- a/package.json +++ b/package.json @@ -267,8 +267,8 @@ "test-unit-mssql": "cross-env DIALECT=mssql npm run test-unit", "test-unit-db2": "cross-env DIALECT=db2 npm run test-unit", "test-unit-snowflake": "cross-env DIALECT=snowflake npm run test-unit", - "test-unit-all": "npm run test-unit-mariadb && npm run test-unit-mysql && npm run test-unit-postgres && npm run test-unit-postgres-native && npm run test-unit-mssql && npm run test-unit-sqlite && npm run test-unit-snowflake && npm run test-unit-db2 && npm run test-unit-oracle", "test-unit-oracle": "cross-env DIALECT=oracle npm run test-unit", + "test-unit-all": "npm run test-unit-mariadb && npm run test-unit-mysql && npm run test-unit-postgres && npm run test-unit-postgres-native && npm run test-unit-mssql && npm run test-unit-sqlite && npm run test-unit-snowflake && npm run test-unit-db2 && npm run test-unit-oracle", "test-integration-mariadb": "cross-env DIALECT=mariadb npm run test-integration", "test-integration-mysql": "cross-env DIALECT=mysql npm run test-integration", "test-integration-postgres": "cross-env DIALECT=postgres npm run test-integration", From 1213de378a3a2c23ddcee751edbbd84fe313736e Mon Sep 17 00:00:00 2001 From: Sudarshan <48428602+sudarshan12s@users.noreply.github.com> Date: Mon, 12 Sep 2022 14:06:34 +0530 Subject: [PATCH 11/14] fix: rename getInsertQueryReturnIntoBinds to populateInsertQueryReturnIntoBinds --- src/dialects/abstract/query-generator.js | 8 ++++---- src/dialects/oracle/query-generator.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dialects/abstract/query-generator.js b/src/dialects/abstract/query-generator.js index a9f7ec929fee..69a3d7a2f964 100644 --- a/src/dialects/abstract/query-generator.js +++ b/src/dialects/abstract/query-generator.js @@ -89,12 +89,12 @@ class QueryGenerator { } /** - * Helper method for getting the returning into bind information + * Helper method for populating the returning into bind information * that is needed by some dialects (currently Oracle) * * @private */ - getInsertQueryReturnIntoBinds() { + populateInsertQueryReturnIntoBinds() { // noop by default } @@ -261,8 +261,8 @@ class QueryGenerator { } if (this._dialect.supports.returnIntoValues && options.returning) { - // Populating the returnAttributes array and performing operations needed for output binds of insertQuery - this.getInsertQueryReturnIntoBinds(returnAttributes, bind.length, returningModelAttributes, returnTypes, options); + // Populating the returnAttributes array and performing operations needed for output binds of insertQuery + this.populateInsertQueryReturnIntoBinds(returnAttributes, bind.length, returningModelAttributes, returnTypes, options); } query = `${replacements.attributes.length ? valueQuery : emptyQuery}${returnAttributes.join(',')};`; diff --git a/src/dialects/oracle/query-generator.js b/src/dialects/oracle/query-generator.js index 828725123004..e6c86aa691b2 100644 --- a/src/dialects/oracle/query-generator.js +++ b/src/dialects/oracle/query-generator.js @@ -544,7 +544,7 @@ export class OracleQueryGenerator extends AbstractQueryGenerator { /** * Populates the returnAttributes array with outbind bindByPosition values - * and also the outBindAttributes map with bindDef for outbind of InsertQuery + * and also the options.outBindAttributes map with bindDef for outbind of InsertQuery * * @param {object} returnAttributes * @param {number} inbindLength @@ -554,7 +554,7 @@ export class OracleQueryGenerator extends AbstractQueryGenerator { * * @private */ - getInsertQueryReturnIntoBinds(returnAttributes, inbindLength, returningModelAttributes, returnTypes, options) { + populateInsertQueryReturnIntoBinds(returnAttributes, inbindLength, returningModelAttributes, returnTypes, options) { const oracledb = this.sequelize.connectionManager.lib; const outBindAttributes = Object.create(null); const outbind = []; From afedbf670c6c81d0c81ca3273132707ab42cd062 Mon Sep 17 00:00:00 2001 From: Sudarshan <48428602+sudarshan12s@users.noreply.github.com> Date: Mon, 12 Sep 2022 16:27:22 +0530 Subject: [PATCH 12/14] fix: reorder parameters for function populateInsertQueryReturnIntoBinds --- src/dialects/abstract/query-generator.js | 2 +- src/dialects/oracle/query-generator.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dialects/abstract/query-generator.js b/src/dialects/abstract/query-generator.js index 69a3d7a2f964..b7329e44dc07 100644 --- a/src/dialects/abstract/query-generator.js +++ b/src/dialects/abstract/query-generator.js @@ -262,7 +262,7 @@ class QueryGenerator { if (this._dialect.supports.returnIntoValues && options.returning) { // Populating the returnAttributes array and performing operations needed for output binds of insertQuery - this.populateInsertQueryReturnIntoBinds(returnAttributes, bind.length, returningModelAttributes, returnTypes, options); + this.populateInsertQueryReturnIntoBinds(returningModelAttributes, returnTypes, bind.length, returnAttributes, options); } query = `${replacements.attributes.length ? valueQuery : emptyQuery}${returnAttributes.join(',')};`; diff --git a/src/dialects/oracle/query-generator.js b/src/dialects/oracle/query-generator.js index e6c86aa691b2..c1d7beaa9114 100644 --- a/src/dialects/oracle/query-generator.js +++ b/src/dialects/oracle/query-generator.js @@ -546,15 +546,15 @@ export class OracleQueryGenerator extends AbstractQueryGenerator { * Populates the returnAttributes array with outbind bindByPosition values * and also the options.outBindAttributes map with bindDef for outbind of InsertQuery * - * @param {object} returnAttributes - * @param {number} inbindLength * @param {Array} returningModelAttributes * @param {Array} returnTypes + * @param {number} inbindLength + * @param {object} returnAttributes * @param {object} options * * @private */ - populateInsertQueryReturnIntoBinds(returnAttributes, inbindLength, returningModelAttributes, returnTypes, options) { + populateInsertQueryReturnIntoBinds(returningModelAttributes, returnTypes, inbindLength, returnAttributes, options) { const oracledb = this.sequelize.connectionManager.lib; const outBindAttributes = Object.create(null); const outbind = []; From 084cd814c5264abd628ce6b6e538e7c28144860c Mon Sep 17 00:00:00 2001 From: Sudarshan Soma <48428602+sudarshan12s@users.noreply.github.com> Date: Wed, 14 Sep 2022 00:53:12 +0530 Subject: [PATCH 13/14] fix: incorporated review comments (#16) * fix: incorporated review comments --- dev/oracle/21-slim/wait-until-healthy.sh | 2 -- src/dialects/abstract/query-generator.js | 2 +- src/dialects/oracle/connection-manager.js | 16 ++++++++++++---- src/dialects/oracle/index.js | 2 +- src/model.js | 4 ++-- .../associations/belongs-to-many.test.js | 7 ++++++- 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/dev/oracle/21-slim/wait-until-healthy.sh b/dev/oracle/21-slim/wait-until-healthy.sh index 53b85e55a37d..096242f82a05 100755 --- a/dev/oracle/21-slim/wait-until-healthy.sh +++ b/dev/oracle/21-slim/wait-until-healthy.sh @@ -1,5 +1,3 @@ -# Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved - #!/usr/bin/env bash if [ "$#" -ne 1 ]; then diff --git a/src/dialects/abstract/query-generator.js b/src/dialects/abstract/query-generator.js index b7329e44dc07..b2f3a0c795ab 100644 --- a/src/dialects/abstract/query-generator.js +++ b/src/dialects/abstract/query-generator.js @@ -1355,7 +1355,7 @@ class QueryGenerator { // For the Oracle dialect, the result of a select is a set, not a sequence, and so is the result of UNION. // So the top level ORDER BY is required - if (this._dialect.name !== 'oracle') { + if (!this._dialect.supports.topLevelOrderByRequired) { delete options.order; } where[Op.placeholder] = true; diff --git a/src/dialects/oracle/connection-manager.js b/src/dialects/oracle/connection-manager.js index bd2c62e1b82c..f79c461b9142 100644 --- a/src/dialects/oracle/connection-manager.js +++ b/src/dialects/oracle/connection-manager.js @@ -25,15 +25,23 @@ export class OracleConnectionManager extends AbstractConnectionManager { this.sequelize = sequelize; this.sequelize.config.port = this.sequelize.config.port || 1521; this.lib = this._loadDialectModule('oracledb'); + this.extendLib(); this.refreshTypeParser(DataTypes); + } + + /** + * Method for initializing the lib + * + */ + extendLib() { this.lib.maxRows = 1000; - if (sequelize.config && 'dialectOptions' in sequelize.config) { - const dialectOptions = sequelize.config.dialectOptions; + if (this.sequelize.config && 'dialectOptions' in this.sequelize.config) { + const dialectOptions = this.sequelize.config.dialectOptions; if (dialectOptions && 'maxRows' in dialectOptions) { - this.lib.maxRows = sequelize.config.dialectOptions.maxRows; + this.lib.maxRows = this.sequelize.config.dialectOptions.maxRows; } if (dialectOptions && 'fetchAsString' in dialectOptions) { - this.lib.fetchAsString = sequelize.config.dialectOptions.fetchAsString; + this.lib.fetchAsString = this.sequelize.config.dialectOptions.fetchAsString; } else { this.lib.fetchAsString = [this.lib.CLOB]; } diff --git a/src/dialects/oracle/index.js b/src/dialects/oracle/index.js index 2514d767fe89..188474f17e56 100644 --- a/src/dialects/oracle/index.js +++ b/src/dialects/oracle/index.js @@ -43,7 +43,6 @@ OracleDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype returnValues: false, returnIntoValues: true, 'ORDER NULLS': true, - ignoreDuplicates: ' IGNORE', schemas: true, updateOnDuplicate: false, indexViaAlter: false, @@ -51,6 +50,7 @@ OracleDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype JSON: true, upserts: true, bulkDefault: true, + topLevelOrderByRequired: true, GEOMETRY: false }); diff --git a/src/model.js b/src/model.js index 10613e40ac1a..39ea6a9fef4b 100644 --- a/src/model.js +++ b/src/model.js @@ -2644,8 +2644,8 @@ class Model { options.returning = true; } } - - if (options.ignoreDuplicates && ['mssql', 'db2', 'oracle'].includes(dialect)) { + if (options.ignoreDuplicates && this.sequelize.dialect.supports.inserts.ignoreDuplicates === '' && + this.sequelize.dialect.supports.inserts.onConflictDoNothing === '') { throw new Error(`${dialect} does not support the ignoreDuplicates option.`); } if (options.updateOnDuplicate && (dialect !== 'mysql' && dialect !== 'mariadb' && dialect !== 'sqlite' && dialect !== 'postgres')) { diff --git a/test/integration/associations/belongs-to-many.test.js b/test/integration/associations/belongs-to-many.test.js index 675e310d8bba..c88c2962efe9 100644 --- a/test/integration/associations/belongs-to-many.test.js +++ b/test/integration/associations/belongs-to-many.test.js @@ -1417,7 +1417,12 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { describe('hasAssociations with binary key', () => { beforeEach(function() { - const keyDataType = ['mysql', 'mariadb', 'db2'].includes(dialect) ? 'BINARY(255)' : dialect === 'oracle' ? DataTypes.STRING(255, true) : DataTypes.BLOB('tiny'); + let keyDataType = DataTypes.BLOB('tiny'); + if (['mysql', 'mariadb', 'db2'].includes(dialect)) { + keyDataType = 'BINARY(255)'; + } else if (dialect === 'oracle') { + keyDataType = DataTypes.STRING(255, true); + } this.Article = this.sequelize.define('Article', { id: { type: keyDataType, From f0a549408db45b35523931efcef986401acd1fc8 Mon Sep 17 00:00:00 2001 From: Sudarshan <48428602+sudarshan12s@users.noreply.github.com> Date: Thu, 15 Sep 2022 09:54:00 +0530 Subject: [PATCH 14/14] fix: modify string empty check with ! --- src/model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/model.js b/src/model.js index 39ea6a9fef4b..ca7a5140705d 100644 --- a/src/model.js +++ b/src/model.js @@ -2644,8 +2644,8 @@ class Model { options.returning = true; } } - if (options.ignoreDuplicates && this.sequelize.dialect.supports.inserts.ignoreDuplicates === '' && - this.sequelize.dialect.supports.inserts.onConflictDoNothing === '') { + if (options.ignoreDuplicates && !this.sequelize.dialect.supports.inserts.ignoreDuplicates && + !this.sequelize.dialect.supports.inserts.onConflictDoNothing) { throw new Error(`${dialect} does not support the ignoreDuplicates option.`); } if (options.updateOnDuplicate && (dialect !== 'mysql' && dialect !== 'mariadb' && dialect !== 'sqlite' && dialect !== 'postgres')) {