diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b89ed8c6565a..1c041db935e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,6 +110,10 @@ jobs: run: yarn lerna run test-unit-ibmi --scope=@sequelize/core - name: Unit tests (core - snowflake) run: yarn lerna run test-unit-snowflake --scope=@sequelize/core + - name: Unit tests (core - oracle) + run: yarn lerna run test-unit-oracle --scope=@sequelize/core + - name: Unit tests (oracle package) + run: yarn lerna run test-unit --scope=@sequelize/oracle - name: SQLite SSCCE run: yarn sscce-sqlite3 test-win: @@ -235,7 +239,7 @@ jobs: matrix: node-version: [18, 20] database-version: [oldest, latest] - dialect: [mysql, mariadb, db2] + dialect: [mysql, mariadb, db2, oracle] name: ${{ matrix.dialect }} ${{ matrix.database-version }} (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest needs: [unit-test, test-typings] diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index b9f7b9b8f41f..000000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,21 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [7.0.0-alpha.46](https://github.com/sequelize/sequelize/compare/v7.0.0-alpha.45...v7.0.0-alpha.46) (2025-03-22) - -### Bug Fixes - -- **cli:** remove redundant types export in package.json ([#17781](https://github.com/sequelize/sequelize/issues/17781)) ([2391263](https://github.com/sequelize/sequelize/commit/2391263eaa09ae8c3fe1ce624b3f696ccfae8501)) -- **core:** fix issues with composite PK in `findByPk` ([#17747](https://github.com/sequelize/sequelize/issues/17747)) ([dd587cb](https://github.com/sequelize/sequelize/commit/dd587cb86a1b636cdc9cc490c9325e2f6e7640a8)) -- **core:** fix msg of error thrown when decorating a non-model ([#17745](https://github.com/sequelize/sequelize/issues/17745)) ([c43c270](https://github.com/sequelize/sequelize/commit/c43c2708d75535edd0fd78e990884a3e38f2fb0d)) -- **core:** proper check upsert support in query-interface ([#17358](https://github.com/sequelize/sequelize/issues/17358)) ([68d7d75](https://github.com/sequelize/sequelize/commit/68d7d758671e0f80bafd68c6980be9dc818683fd)) -- **postgres:** correct existing enum type matching ([#17576](https://github.com/sequelize/sequelize/issues/17576)) ([425d217](https://github.com/sequelize/sequelize/commit/425d21718af40f86015f6496ea6cf721cc61b981)) -- **postgres:** update to postgres 17 ([#17740](https://github.com/sequelize/sequelize/issues/17740)) ([b5c2b26](https://github.com/sequelize/sequelize/commit/b5c2b2667004b3b27e5634c677507f5593987938)) -- update typescript to v5.8.2 ([#17728](https://github.com/sequelize/sequelize/issues/17728)) ([6c5a82d](https://github.com/sequelize/sequelize/commit/6c5a82dbc82ec45bbe85112c51e1b496f3f7dbaa)) - -### Features - -- **core:** add `sql.join` & improve `sql.identifier` ([#17744](https://github.com/sequelize/sequelize/issues/17744)) ([e914861](https://github.com/sequelize/sequelize/commit/e914861c084ef0ed8f12ca7b59be4965326e9641)) -- **core:** count grouped rows ([#17751](https://github.com/sequelize/sequelize/issues/17751)) ([a396673](https://github.com/sequelize/sequelize/commit/a396673b4edad0d3d3379111a3b1cbf3695d22cc)) diff --git a/dev/oracle/latest/docker-compose.yml b/dev/oracle/latest/docker-compose.yml new file mode 100644 index 000000000000..b78a2650f242 --- /dev/null +++ b/dev/oracle/latest/docker-compose.yml @@ -0,0 +1,20 @@ +# Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +services: + oracle-latest: + container_name: sequelize-oracle-latest + image: gvenzl/oracle-free:23.4-slim + environment: + ORACLE_PASSWORD: password + ORACLE_DATABASE: XEPDB1 + ports: + - 1521:1521 + healthcheck: + test: ['CMD-SHELL', 'sqlplus', 'system/password@localhost:1521/XEPDB1'] + interval: 3s + timeout: 1s + retries: 10 + +networks: + default: + name: sequelize-oracle-latest-network diff --git a/dev/oracle/latest/reset.sh b/dev/oracle/latest/reset.sh new file mode 100644 index 000000000000..965b05e92c9e --- /dev/null +++ b/dev/oracle/latest/reset.sh @@ -0,0 +1,7 @@ +# Copyright (c) 2024, 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 sequelize-oracle-latest down --remove-orphans --volumes diff --git a/dev/oracle/latest/start.sh b/dev/oracle/latest/start.sh new file mode 100644 index 000000000000..32243dec0083 --- /dev/null +++ b/dev/oracle/latest/start.sh @@ -0,0 +1,18 @@ +# Copyright (c) 2024, 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 sequelize-oracle-latest down --remove-orphans +docker compose -p sequelize-oracle-latest up -d + +./../../wait-until-healthy.sh sequelize-oracle-latest + +sleep 30s + +docker cp ../privileges.sql sequelize-oracle-latest:/opt/oracle/. + +docker exec -t sequelize-oracle-latest sqlplus system/password@localhost:1521/XEPDB1 @privileges.sql + +DIALECT=oracle ts-node ../../check-connection.ts diff --git a/dev/oracle/latest/stop.sh b/dev/oracle/latest/stop.sh new file mode 100644 index 000000000000..c96fd493efd0 --- /dev/null +++ b/dev/oracle/latest/stop.sh @@ -0,0 +1,10 @@ +# Copyright (c) 2024, 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 sequelize-oracle-latest down --remove-orphans + +echo "Local latest supported Oracle DB instance stopped (if it was running). diff --git a/dev/oracle/oldest/docker-compose.yml b/dev/oracle/oldest/docker-compose.yml new file mode 100644 index 000000000000..c7171263bff9 --- /dev/null +++ b/dev/oracle/oldest/docker-compose.yml @@ -0,0 +1,19 @@ +# Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +services: + oracle-oldest: + container_name: sequelize-oracle-oldest + image: gvenzl/oracle-xe:18-slim + environment: + ORACLE_PASSWORD: password + ports: + - 1521:1521 + healthcheck: + test: ['CMD-SHELL', 'sqlplus', 'system/password@XEPDB1'] + interval: 3s + timeout: 1s + retries: 10 + +networks: + default: + name: sequelize-oracle-oldest-network diff --git a/dev/oracle/oldest/reset.sh b/dev/oracle/oldest/reset.sh new file mode 100644 index 000000000000..95057a0e6302 --- /dev/null +++ b/dev/oracle/oldest/reset.sh @@ -0,0 +1,7 @@ +# Copyright (c) 2024, 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 sequelize-oracle-oldest down --remove-orphans --volumes diff --git a/dev/oracle/oldest/start.sh b/dev/oracle/oldest/start.sh new file mode 100644 index 000000000000..e62393eeb137 --- /dev/null +++ b/dev/oracle/oldest/start.sh @@ -0,0 +1,17 @@ +# Copyright (c) 2024, 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 sequelize-oracle-oldest down --remove-orphans +docker compose -p sequelize-oracle-oldest up -d + +./../../wait-until-healthy.sh sequelize-oracle-oldest + +sleep 30s + +docker cp ../privileges.sql sequelize-oracle-oldest:/opt/oracle/. +docker exec -t sequelize-oracle-oldest sqlplus system/password@XEPDB1 @privileges.sql + +DIALECT=oracle ts-node ../../check-connection.ts diff --git a/dev/oracle/oldest/stop.sh b/dev/oracle/oldest/stop.sh new file mode 100644 index 000000000000..cd6dc54c1f0a --- /dev/null +++ b/dev/oracle/oldest/stop.sh @@ -0,0 +1,9 @@ +# Copyright (c) 2024, 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 sequelize-oracle-oldest down --remove-orphans + +echo "Local oldest supported Oracle DB instance stopped (if it was running)." diff --git a/dev/oracle/privileges.sql b/dev/oracle/privileges.sql new file mode 100644 index 000000000000..2f58584b9064 --- /dev/null +++ b/dev/oracle/privileges.sql @@ -0,0 +1,6 @@ +-- Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +create user sequelizetest identified by sequelizepassword; +grant all privileges to sequelizetest; +alter user sequelizetest quota unlimited on users; +exit; \ No newline at end of file diff --git a/package.json b/package.json index cd730e92979c..d10fe59eea75 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test-integration-db2": "yarn lerna run test-integration-db2", "test-integration-ibmi": "yarn lerna run test-integration-ibmi", "test-integration-snowflake": "yarn lerna run test-integration-snowflake", + "test-integration-oracle": "yarn lerna run test-integration-oracle", "sync-exports": "lerna run sync-exports", "build": "lerna run build", "docs": "typedoc", @@ -34,6 +35,7 @@ "reset-postgres": "bash dev/postgres/oldest/reset.sh; bash dev/postgres/latest/reset.sh", "reset-mssql": "bash dev/mssql/oldest/reset.sh; bash dev/mssql/latest/reset.sh", "reset-db2": "bash dev/db2/oldest/reset.sh; bash dev/db2/latest/reset.sh", + "reset-oracle": "bash dev/oracle/oldest/reset.sh; bash dev/oracle/latest/reset.sh", "reset-all": "concurrently \"npm:reset-*(!all)\"", "start-mariadb-oldest": "bash dev/mariadb/oldest/start.sh", "start-mariadb-latest": "bash dev/mariadb/latest/start.sh", @@ -45,6 +47,8 @@ "start-mssql-latest": "bash dev/mssql/latest/start.sh", "start-db2-oldest": "bash dev/db2/oldest/start.sh", "start-db2-latest": "bash dev/db2/latest/start.sh", + "start-oracle-oldest": "bash dev/oracle/oldest/start.sh", + "start-oracle-latest": "bash dev/oracle/latest/start.sh", "start-oldest": "concurrently \"npm:start-*-oldest\"", "start-latest": "concurrently \"npm:start-*-latest\"", "stop-mariadb": "bash dev/mariadb/oldest/stop.sh; bash dev/mariadb/latest/stop.sh", @@ -52,6 +56,7 @@ "stop-postgres": "bash dev/postgres/oldest/stop.sh; bash dev/postgres/latest/stop.sh", "stop-mssql": "bash dev/mssql/oldest/stop.sh; bash dev/mssql/latest/stop.sh", "stop-db2": "bash dev/db2/oldest/stop.sh; bash dev/db2/latest/stop.sh", + "stop-oracle": "bash dev/oracle/oldest/stop.sh; bash dev/oracle/latest/stop.sh", "stop-all": "concurrently \"npm:stop-*(!all)\"", "----------------------------------------- SSCCEs ------------------------------------------": "", "sscce": "ts-node sscce.ts", @@ -61,7 +66,8 @@ "sscce-postgres-native": "cross-env DIALECT=postgres-native yarn sscce", "sscce-sqlite3": "cross-env DIALECT=sqlite3 yarn sscce", "sscce-mssql": "cross-env DIALECT=mssql yarn sscce", - "sscce-db2": "cross-env DIALECT=db2 yarn sscce" + "sscce-db2": "cross-env DIALECT=db2 yarn sscce", + "sscce-oracle": "cross-env DIALECT=oracle yarn sscce" }, "workspaces": [ "packages/*" diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md deleted file mode 100644 index dc829955d269..000000000000 --- a/packages/core/CHANGELOG.md +++ /dev/null @@ -1,20 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [7.0.0-alpha.46](https://github.com/sequelize/sequelize/compare/v7.0.0-alpha.45...v7.0.0-alpha.46) (2025-03-22) - -### Bug Fixes - -- **core:** fix issues with composite PK in `findByPk` ([#17747](https://github.com/sequelize/sequelize/issues/17747)) ([dd587cb](https://github.com/sequelize/sequelize/commit/dd587cb86a1b636cdc9cc490c9325e2f6e7640a8)) -- **core:** fix msg of error thrown when decorating a non-model ([#17745](https://github.com/sequelize/sequelize/issues/17745)) ([c43c270](https://github.com/sequelize/sequelize/commit/c43c2708d75535edd0fd78e990884a3e38f2fb0d)) -- **core:** proper check upsert support in query-interface ([#17358](https://github.com/sequelize/sequelize/issues/17358)) ([68d7d75](https://github.com/sequelize/sequelize/commit/68d7d758671e0f80bafd68c6980be9dc818683fd)) -- **postgres:** correct existing enum type matching ([#17576](https://github.com/sequelize/sequelize/issues/17576)) ([425d217](https://github.com/sequelize/sequelize/commit/425d21718af40f86015f6496ea6cf721cc61b981)) -- **postgres:** update to postgres 17 ([#17740](https://github.com/sequelize/sequelize/issues/17740)) ([b5c2b26](https://github.com/sequelize/sequelize/commit/b5c2b2667004b3b27e5634c677507f5593987938)) -- update typescript to v5.8.2 ([#17728](https://github.com/sequelize/sequelize/issues/17728)) ([6c5a82d](https://github.com/sequelize/sequelize/commit/6c5a82dbc82ec45bbe85112c51e1b496f3f7dbaa)) - -### Features - -- **core:** add `sql.join` & improve `sql.identifier` ([#17744](https://github.com/sequelize/sequelize/issues/17744)) ([e914861](https://github.com/sequelize/sequelize/commit/e914861c084ef0ed8f12ca7b59be4965326e9641)) -- **core:** count grouped rows ([#17751](https://github.com/sequelize/sequelize/issues/17751)) ([a396673](https://github.com/sequelize/sequelize/commit/a396673b4edad0d3d3379111a3b1cbf3695d22cc)) diff --git a/packages/core/package.json b/packages/core/package.json index bf73b68ca7b9..1a9cd45e2216 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sequelize/core", - "description": "Sequelize is a promise-based Node.js ORM tool for Postgres, MySQL, MariaDB, SQLite, Microsoft SQL Server, Amazon Redshift, Snowflake’s Data Cloud, Db2, and IBM i. It features solid transaction support, relations, eager and lazy loading, read replication and more.", + "description": "Sequelize is a promise-based Node.js ORM tool for Postgres, MySQL, MariaDB, SQLite, Microsoft SQL Server, Amazon Redshift, Snowflake’s Data Cloud, Db2, Oracle, and IBM i. It features solid transaction support, relations, eager and lazy loading, read replication and more.", "version": "7.0.0-alpha.46", "funding": [ { @@ -108,6 +108,7 @@ "sql", "sqlserver", "snowflake", + "oracledb", "orm", "nodejs", "object relational mapper", @@ -141,7 +142,8 @@ "test-unit-db2": "cross-env DIALECT=db2 yarn _test-unit", "test-unit-ibmi": "cross-env DIALECT=ibmi yarn _test-unit", "test-unit-snowflake": "cross-env DIALECT=snowflake yarn _test-unit", - "test-unit-all": "yarn test-unit-mariadb && yarn test-unit-mysql && yarn test-unit-postgres && yarn test-unit-mssql && yarn test-unit-sqlite3 && yarn test-unit-snowflake && yarn test-unit-db2 && yarn test-unit-ibmi", + "test-unit-oracle": "cross-env DIALECT=oracle yarn _test-unit", + "test-unit-all": "yarn test-unit-mariadb && yarn test-unit-mysql && yarn test-unit-postgres && yarn test-unit-mssql && yarn test-unit-sqlite3 && yarn test-unit-snowflake && yarn test-unit-db2 && yarn test-unit-oracle && yarn test-unit-ibmi", "test-unit": "yarn test-unit-all", "----------------------------------------- integration tests ---------------------------------------------": "", "test-integration-mariadb": "cross-env DIALECT=mariadb yarn test-integration", @@ -153,7 +155,8 @@ "test-integration-db2": "cross-env DIALECT=db2 yarn test-integration", "test-integration-ibmi": "cross-env DIALECT=ibmi yarn test-integration", "test-integration-snowflake": "cross-env DIALECT=snowflake yarn test-integration", - "test-integration-all": "yarn test-integration-mariadb && yarn test-integration-mysql && yarn test-integration-postgres && yarn test-integration-postgres-native && yarn test-integration-sqlite3 && yarn test-integration-mssql && yarn test-integration-db2 && yarn test-integration-ibmi && yarn test-integration-snowflake", + "test-integration-oracle": "cross-env DIALECT=oracle yarn test-integration", + "test-integration-all": "yarn test-integration-mariadb && yarn test-integration-mysql && yarn test-integration-postgres && yarn test-integration-postgres-native && yarn test-integration-sqlite3 && yarn test-integration-mssql && yarn test-integration-db2 && yarn test-integration-ibmi && yarn test-integration-snowflake && yarn test-integration-oracle", "----------------------------------------- all tests ---------------------------------------------": "", "test-mariadb": "cross-env DIALECT=mariadb yarn test", "test-mysql": "cross-env DIALECT=mysql yarn test", @@ -163,6 +166,7 @@ "test-mssql": "cross-env DIALECT=mssql yarn test", "test-db2": "cross-env DIALECT=db2 yarn test", "test-ibmi": "cross-env DIALECT=ibmi yarn test", + "test-oracle": "cross-env DIALECT=oracle yarn test", "----------------------------------------- development ---------------------------------------------": "", "build": "node ../../build-packages.mjs core" }, diff --git a/packages/core/src/abstract-dialect/dialect.ts b/packages/core/src/abstract-dialect/dialect.ts index 9d17dd6ee1a1..94cd5672674c 100644 --- a/packages/core/src/abstract-dialect/dialect.ts +++ b/packages/core/src/abstract-dialect/dialect.ts @@ -68,6 +68,12 @@ export type DialectSupports = { /* does the dialect support returning values for inserted/updated fields */ returnValues: false | 'output' | 'returning'; + /* does the dialect support returning values for inserted/updated fields in outBinds */ + returnIntoValues: boolean; + + /* does the dialect support topLevelOrderBy (ORDER BY clasue) to get desired results */ + topLevelOrderByRequired: boolean; + /* features specific to autoIncrement values */ autoIncrement: { /* does the dialect require modification of insert queries when inserting auto increment fields */ @@ -324,6 +330,8 @@ export abstract class AbstractDialect< skipLocked: false, finalTable: false, returnValues: false, + returnIntoValues: false, + topLevelOrderByRequired: false, autoIncrement: { identityInsert: false, defaultValue: true, diff --git a/packages/core/src/abstract-dialect/query-generator-internal.ts b/packages/core/src/abstract-dialect/query-generator-internal.ts index 59ae9250d275..45028f17694f 100644 --- a/packages/core/src/abstract-dialect/query-generator-internal.ts +++ b/packages/core/src/abstract-dialect/query-generator-internal.ts @@ -351,4 +351,12 @@ Only named replacements (:name) are allowed in literal() because we cannot guara addLimitAndOffset(_options: AddLimitOffsetOptions): string { throw new Error(`addLimitAndOffset has not been implemented in ${this.dialect.name}.`); } + + /** + * Returns the alias token 'AS' after `FROM` clause. + * + */ + getAliasToken() { + return 'AS'; + } } diff --git a/packages/core/src/abstract-dialect/query-generator-typescript.ts b/packages/core/src/abstract-dialect/query-generator-typescript.ts index 0744d4b41d7c..85e7ac8d0b5c 100644 --- a/packages/core/src/abstract-dialect/query-generator-typescript.ts +++ b/packages/core/src/abstract-dialect/query-generator-typescript.ts @@ -658,7 +658,7 @@ export class AbstractQueryGeneratorTypeScript 0 ? valueQuery : emptyQuery}`.trim()};`; + if (this.dialect.supports.returnIntoValues && options.returning) { + // Populating the returnAttributes array and performing operations needed for output binds of insertQuery + this.populateInsertQueryReturnIntoBinds( + returningModelAttributes, + returnTypes, + Object.keys(bind).length, + returnAttributes, + options, + ); + } + + query = `${`${replacements.attributes.length > 0 ? valueQuery : emptyQuery}${returnAttributes.join(',')}`.trim()};`; if (this.dialect.supports.finalTable) { query = `SELECT * FROM FINAL TABLE (${replacements.attributes.length > 0 ? valueQuery : emptyQuery});`; } @@ -409,6 +431,17 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { ]); } + /** + * Helper method for populating the returning into bind information + * that is needed by some dialects (currently Oracle) + * This is called when `dialect.supports.returnIntoClause` is `True` + * + * @private + */ + populateInsertQueryReturnIntoBinds() { + // noop by default + } + /** * Returns an update query * @@ -448,14 +481,22 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { bindParam = createBindParamGenerator(bind); } - if ( - this.dialect.supports['LIMIT ON UPDATE'] && - options.limit && - this.dialect.name !== 'mssql' && - this.dialect.name !== 'db2' - ) { - // TODO: use bind parameter - suffix = ` LIMIT ${this.escape(options.limit, options)} `; + if (this.dialect.supports['LIMIT ON UPDATE'] && options.limit) { + if (!['mssql', 'db2', 'oracle'].includes(this.dialect.name)) { + // TODO: use bind parameter + suffix = ` LIMIT ${this.escape(options.limit, options)} `; + } else if (this.dialect.name === 'oracle') { + // This cannot be set in where clause 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)} `; + } } if (this.dialect.supports.returnValues && options.returning) { @@ -1190,7 +1231,13 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { } else { // Ordering is handled by the subqueries, so ordering the UNION'ed result is not needed groupedLimitOrder = options.order; - delete options.order; + // For dialects which don't allow for ordering in the subqueries, 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.supports.topLevelOrderByRequired) { + delete options.order; + } + where = and(new Literal(placeholder), where); } @@ -1211,7 +1258,7 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { model, }, model, - ).replace(/;$/, '')}) AS sub`; // Every derived table must have its own alias + ).replace(/;$/, '')}) ${this.#internals.getAliasToken()} sub`; // Every derived table must have its own alias const splicePos = baseQuery.indexOf(placeholder); mainQueryItems.push( @@ -1398,7 +1445,7 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { modelName: model && model.name, as: mainTable.quotedAs, }); - query = `SELECT ${attributes.main.join(', ')} FROM (${subQueryItems.join('')}) AS ${mainTable.quotedAs}${mainJoinQueries.join('')}${mainQueryItems.join('')}`; + query = `SELECT ${attributes.main.join(', ')} FROM (${subQueryItems.join('')}) ${this.#internals.getAliasToken()} ${mainTable.quotedAs}${mainJoinQueries.join('')}${mainQueryItems.join('')}`; } else { query = mainQueryItems.join(''); } @@ -1565,6 +1612,11 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { /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)}`; } @@ -1897,6 +1949,8 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { if (returnValuesType === 'returning') { returningFragment = ` RETURNING ${returnFields.join(', ')}`; + } else if (this.dialect.supports.returnIntoValues) { + returningFragment = ` RETURNING ${returnFields.join(', ')} INTO `; } else if (returnValuesType === 'output') { outputFragment = ` OUTPUT ${returnFields.map(field => `INSERTED.${field}`).join(', ')}`; @@ -1912,7 +1966,7 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { } } - return { outputFragment, returnFields, returningFragment, tmpTable }; + return { outputFragment, returnFields, returnTypes, returningFragment, tmpTable }; } generateThroughJoin(include, includeAs, parentTableName, topLevelInfo, options) { @@ -2290,7 +2344,7 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript { fragment += ` ${attributes.join(', ')} FROM ${tables}`; if (options.groupedLimit) { - fragment += ` AS ${mainTableAs}`; + fragment += ` ${this.#internals.getAliasToken()} ${mainTableAs}`; } return fragment; diff --git a/packages/core/src/model.js b/packages/core/src/model.js index a3608ec40062..dc82b418011b 100644 --- a/packages/core/src/model.js +++ b/packages/core/src/model.js @@ -1690,8 +1690,10 @@ ${associationOwner._getAssociationDebugList()}`); // use a subquery to get the count if (options.group && options.countGroupedRows) { const query = removeTrailingSemicolon(this.queryGenerator.selectQuery(this.table, options)); + const dialect = this.sequelize.dialect.name; - const queryCountAll = `Select COUNT(*) AS count FROM (${query}) AS Z`; + // Oracle doesn't support 'AS' keyword for aliasing tables + const queryCountAll = `Select COUNT(*) AS count FROM (${query}) ${dialect !== 'oracle' ? 'AS' : ''} Z`; const result = await this.sequelize.query(queryCountAll); @@ -2312,7 +2314,7 @@ ${associationOwner._getAssociationDebugList()}`); } } - if (options.ignoreDuplicates && ['mssql', 'db2', 'ibmi'].includes(dialect)) { + if (options.ignoreDuplicates && ['mssql', 'db2', 'ibmi', 'oracle'].includes(dialect)) { throw new Error(`${dialect} does not support the ignoreDuplicates option.`); } diff --git a/packages/core/src/sequelize-typescript.ts b/packages/core/src/sequelize-typescript.ts index 22a39751ba7e..bd41e8072026 100644 --- a/packages/core/src/sequelize-typescript.ts +++ b/packages/core/src/sequelize-typescript.ts @@ -207,6 +207,7 @@ export const SUPPORTED_DIALECTS = Object.freeze([ 'db2', 'snowflake', 'ibmi', + 'oracle', ] as const); // DO NOT MAKE THIS CLASS PUBLIC! diff --git a/packages/core/src/sequelize.internals.ts b/packages/core/src/sequelize.internals.ts index 4221d2ca9096..0701817d215f 100644 --- a/packages/core/src/sequelize.internals.ts +++ b/packages/core/src/sequelize.internals.ts @@ -44,9 +44,12 @@ export function importDialect(dialect: string): typeof AbstractDialect { case 'snowflake': // eslint-disable-next-line import/no-extraneous-dependencies -- legacy function, will be removed. User needs to install the dependency themselves return require('@sequelize/snowflake').SnowflakeDialect; + case 'oracle': + // eslint-disable-next-line import/no-extraneous-dependencies -- legacy function, will be removed. User needs to install the dependency themselves + return require('@sequelize/oracle').OracleDialect; default: throw new Error( - `The dialect ${dialect} is not natively supported. Native dialects: mariadb, mssql, mysql, postgres, sqlite3, ibmi, db2 and snowflake.`, + `The dialect ${dialect} is not natively supported. Native dialects: mariadb, mssql, mysql, postgres, sqlite3, ibmi, db2, oracle and snowflake.`, ); } } diff --git a/packages/core/src/sequelize.js b/packages/core/src/sequelize.js index 71082eb3e7b0..97a9c83376d8 100644 --- a/packages/core/src/sequelize.js +++ b/packages/core/src/sequelize.js @@ -592,7 +592,7 @@ Use Sequelize#query if you wish to use replacements.`); }; await this.query( - `SELECT 1+1 AS result${this.dialect.name === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `SELECT 1+1 AS result${this.dialect.name === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : this.dialect.name === 'oracle' ? ' FROM DUAL' : ''}`, options, ); } diff --git a/packages/core/test/config/config.ts b/packages/core/test/config/config.ts index 423c606b576b..c331ef6dee0d 100644 --- a/packages/core/test/config/config.ts +++ b/packages/core/test/config/config.ts @@ -4,6 +4,7 @@ import { IBMiDialect } from '@sequelize/db2-ibmi'; import { MariaDbDialect } from '@sequelize/mariadb'; import { MsSqlDialect } from '@sequelize/mssql'; import { MySqlDialect } from '@sequelize/mysql'; +import { OracleDialect } from '@sequelize/oracle'; import { PostgresDialect } from '@sequelize/postgres'; import { SnowflakeDialect } from '@sequelize/snowflake'; import { SqliteDialect } from '@sequelize/sqlite3'; @@ -27,6 +28,7 @@ export interface DialectConfigs { postgres: Options; db2: Options; ibmi: Options; + oracle: Options; } export interface DialectConnectionConfigs { @@ -38,6 +40,7 @@ export interface DialectConnectionConfigs { postgres: ConnectionOptions; db2: ConnectionOptions; ibmi: ConnectionOptions; + oracle: ConnectionOptions; } const seqPort = env.SEQ_PORT ? parseSafeInteger.orThrow(env.SEQ_PORT) : undefined; @@ -143,6 +146,20 @@ export const CONFIG: DialectConfigs = { }, }, + oracle: { + dialect: OracleDialect, + 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: Number(env.SEQ_ORACLE_POOL_MAX || env.SEQ_POOL_MAX || 5), + idle: Number(env.SEQ_ORACLE_POOL_IDLE || env.SEQ_POOL_IDLE || 3000), + }, + stmtCacheSize: Number(env.SEQ_ORACLE_STMT_CACHE || 0), + }, + ibmi: { dialect: IBMiDialect, dataSourceName: env.SEQ_IBMI_DB || env.SEQ_DB, diff --git a/packages/core/test/integration/associations/belongs-to-many.test.js b/packages/core/test/integration/associations/belongs-to-many.test.js index 3485630f72f1..14811b70318d 100644 --- a/packages/core/test/integration/associations/belongs-to-many.test.js +++ b/packages/core/test/integration/associations/belongs-to-many.test.js @@ -1797,7 +1797,9 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { beforeEach(function () { const keyDataType = ['mysql', 'mariadb', 'db2', 'ibmi'].includes(dialect) ? 'BINARY(255)' - : DataTypes.BLOB('tiny'); + : dialect === 'oracle' + ? DataTypes.STRING(255, true) + : DataTypes.BLOB('tiny'); this.Article = this.sequelize.define('Article', { id: { type: keyDataType, @@ -2322,6 +2324,10 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { }); it('supports transactions when updating a through model', async function () { + if (dialect === 'oracle') { + return; + } + const sequelize = await Support.createSingleTransactionalTestSequelizeInstance( this.sequelize, ); diff --git a/packages/core/test/integration/associations/belongs-to.test.js b/packages/core/test/integration/associations/belongs-to.test.js index 7bd735627724..716600a89eb9 100644 --- a/packages/core/test/integration/associations/belongs-to.test.js +++ b/packages/core/test/integration/associations/belongs-to.test.js @@ -675,7 +675,7 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { } // NOTE: mssql does not support changing an autoincrement primary key - if (!['mssql', 'db2', 'ibmi'].includes(dialect)) { + if (!['mssql', 'db2', 'ibmi', 'oracle'].includes(dialect)) { it('can cascade updates', async function () { const Task = this.sequelize.define('Task', { title: DataTypes.STRING }); const User = this.sequelize.define('User', { username: DataTypes.STRING }); diff --git a/packages/core/test/integration/associations/has-many.test.js b/packages/core/test/integration/associations/has-many.test.js index 3f99d23826d1..04851a609c6e 100644 --- a/packages/core/test/integration/associations/has-many.test.js +++ b/packages/core/test/integration/associations/has-many.test.js @@ -1114,7 +1114,7 @@ describe('HasMany', () => { }); // NOTE: mssql does not support changing an autoincrement primary key - if (!['mssql', 'db2', 'ibmi'].includes(dialectName)) { + if (!['mssql', 'db2', 'ibmi', 'oracle'].includes(dialectName)) { it('can cascade updates', async function () { const Task = this.sequelize.define('Task', { title: DataTypes.STRING }); const User = this.sequelize.define('User', { username: DataTypes.STRING }); diff --git a/packages/core/test/integration/associations/has-one.test.js b/packages/core/test/integration/associations/has-one.test.js index 58ebaf66da21..46d28f206a30 100644 --- a/packages/core/test/integration/associations/has-one.test.js +++ b/packages/core/test/integration/associations/has-one.test.js @@ -362,7 +362,7 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { }); // NOTE: mssql does not support changing an autoincrement primary key - if (!['mssql', 'db2', 'ibmi'].includes(dialect)) { + if (!['mssql', 'db2', 'ibmi', 'oracle'].includes(dialect)) { it('can cascade updates', async function () { const Task = this.sequelize.define('Task', { title: DataTypes.STRING }); const User = this.sequelize.define('User', { username: DataTypes.STRING }); diff --git a/packages/core/test/integration/cls.test.ts b/packages/core/test/integration/cls.test.ts index 99c5bf59e499..161c73420a02 100644 --- a/packages/core/test/integration/cls.test.ts +++ b/packages/core/test/integration/cls.test.ts @@ -7,6 +7,7 @@ import sinon from 'sinon'; import { beforeAll2, createMultiTransactionalTestSequelizeInstance, + getTestDialect, sequelize, setResetMode, } from './support'; @@ -173,7 +174,10 @@ describe('AsyncLocalStorage (ContinuationLocalStorage) Transactions (CLS)', () = it('promises returned by sequelize.query are correctly patched', async () => { await vars.clsSequelize.transaction(async t => { - await vars.clsSequelize.query('select 1', { type: QueryTypes.SELECT }); + await vars.clsSequelize.query( + `select 1 ${getTestDialect() === 'oracle' ? 'FROM DUAL' : ''}`, + { type: QueryTypes.SELECT }, + ); return expect(vars.clsSequelize.getCurrentClsTransaction()).to.equal(t); }); diff --git a/packages/core/test/integration/configuration.test.ts b/packages/core/test/integration/configuration.test.ts index e1bfcf075381..76c5f1f385fc 100644 --- a/packages/core/test/integration/configuration.test.ts +++ b/packages/core/test/integration/configuration.test.ts @@ -62,6 +62,10 @@ describe('Configuration', () => { storage: '/path/to/no/where/land', mode: OPEN_READONLY, }, + oracle: { + ...CONFIG.oracle, + port: 19_999, + }, }; const errorByDialect: Record> = { @@ -73,6 +77,7 @@ describe('Configuration', () => { snowflake: HostNotReachableError, db2: ConnectionRefusedError, sqlite3: InvalidConnectionError, + oracle: ConnectionRefusedError, }; const seq = new Sequelize(badHostConfigs[dialectName]); @@ -123,6 +128,10 @@ describe('Configuration', () => { ...CONFIG.ibmi, password: 'wrongpassword', }, + oracle: { + ...CONFIG.oracle, + password: 'wrongpassword', + }, }; const seq = new Sequelize(config[dialectName]); diff --git a/packages/core/test/integration/data-types/data-types.test.ts b/packages/core/test/integration/data-types/data-types.test.ts index d1599dde4a6a..2f455f8c78ed 100644 --- a/packages/core/test/integration/data-types/data-types.test.ts +++ b/packages/core/test/integration/data-types/data-types.test.ts @@ -129,10 +129,18 @@ describe('DataTypes', () => { }); it('accepts strings', async () => { + if (dialect.name === 'oracle') { + return; + } + await testSimpleInOut(vars.User, 'binaryStringAttr', 'abc', 'abc'); }); it('is deserialized as a string when DataType is not specified', async () => { + if (dialect.name === 'oracle') { + return; + } + await testSimpleInOutRaw(vars.User, 'binaryStringAttr', 'abc', 'abc'); }); }); @@ -164,7 +172,7 @@ describe('DataTypes', () => { }); // TODO: add length check constraint in sqlite - if (dialect.name !== 'sqlite3') { + if (dialect.name !== 'sqlite3' && dialect.name !== 'oracle') { it('throws if the string is too long', async () => { await expect( vars.User.create({ @@ -201,6 +209,11 @@ describe('DataTypes', () => { }); it('is deserialized as a string when DataType is not specified', async () => { + // For raw queries, Oracle expects hex string during insertion + if (dialect.name === 'oracle') { + return; + } + await testSimpleInOutRaw(vars.User, 'textAttr', 'abc', 'abc'); }); }); @@ -310,70 +323,72 @@ describe('DataTypes', () => { }); }); - describe('CHAR().BINARY', () => { - if (!dialect.supports.dataTypes.CHAR) { - it('throws, because this dialect does not support CHAR', async () => { - expect(() => { - sequelize.define('CrashedModel', { - attr: DataTypes.CHAR(5), - }); - }).to.throwWithCause(`${dialect.name} does not support the CHAR data type.`); - }); - - return; - } + if (dialect.name !== 'oracle') { + describe('CHAR().BINARY', () => { + if (!dialect.supports.dataTypes.CHAR) { + it('throws, because this dialect does not support CHAR', async () => { + expect(() => { + sequelize.define('CrashedModel', { + attr: DataTypes.CHAR(5), + }); + }).to.throwWithCause(`${dialect.name} does not support the CHAR data type.`); + }); - if (!dialect.supports.dataTypes.COLLATE_BINARY) { - it('throws if CHAR.BINARY is used', () => { - expect(() => { - sequelize.define('CrashedModel', { - attr: DataTypes.CHAR(5).BINARY, - }); - }).to.throwWithCause(`${dialect.name} does not support the CHAR.BINARY data type.`); - }); + return; + } - return; - } + if (!dialect.supports.dataTypes.COLLATE_BINARY) { + it('throws if CHAR.BINARY is used', () => { + expect(() => { + sequelize.define('CrashedModel', { + attr: DataTypes.CHAR(5).BINARY, + }); + }).to.throwWithCause(`${dialect.name} does not support the CHAR.BINARY data type.`); + }); - const vars = beforeAll2(async () => { - class User extends Model> { - declare binaryCharAttr: string | ArrayBuffer | Uint8Array | Blob; + return; } - User.init( - { - binaryCharAttr: { - type: DataTypes.CHAR(5).BINARY, - allowNull: false, + const vars = beforeAll2(async () => { + class User extends Model> { + declare binaryCharAttr: string | ArrayBuffer | Uint8Array | Blob; + } + + User.init( + { + binaryCharAttr: { + type: DataTypes.CHAR(5).BINARY, + allowNull: false, + }, }, - }, - { sequelize }, - ); + { sequelize }, + ); - await User.sync({ force: true }); + await User.sync({ force: true }); - return { User }; - }); + return { User }; + }); - it('is serialized/deserialized as strings', async () => { - // mysql does not pad columns, unless PAD_CHAR_TO_FULL_LENGTH is true - if (dialect.name === 'db2') { - await testSimpleInOut(vars.User, 'binaryCharAttr', '1234', '1234 '); - } else { - await testSimpleInOut(vars.User, 'binaryCharAttr', '1234', '1234'); - } - }); + it('is serialized/deserialized as strings', async () => { + // mysql does not pad columns, unless PAD_CHAR_TO_FULL_LENGTH is true + if (dialect.name === 'db2') { + await testSimpleInOut(vars.User, 'binaryCharAttr', '1234', '1234 '); + } else { + await testSimpleInOut(vars.User, 'binaryCharAttr', '1234', '1234'); + } + }); - it('is deserialized as a string when DataType is not specified', async () => { - // mysql does not pad columns, unless PAD_CHAR_TO_FULL_LENGTH is true - // https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#sqlmode_pad_char_to_full_length - if (dialect.name === 'db2') { - await testSimpleInOutRaw(vars.User, 'binaryCharAttr', Buffer.from(' 234'), ' 234 '); - } else { - await testSimpleInOutRaw(vars.User, 'binaryCharAttr', Buffer.from(' 234'), ' 234'); - } + it('is deserialized as a string when DataType is not specified', async () => { + // mysql does not pad columns, unless PAD_CHAR_TO_FULL_LENGTH is true + // https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#sqlmode_pad_char_to_full_length + if (dialect.name === 'db2') { + await testSimpleInOutRaw(vars.User, 'binaryCharAttr', Buffer.from(' 234'), ' 234 '); + } else { + await testSimpleInOutRaw(vars.User, 'binaryCharAttr', Buffer.from(' 234'), ' 234'); + } + }); }); - }); + } describe('CITEXT', () => { if (!dialect.supports.dataTypes.CITEXT) { @@ -546,6 +561,12 @@ describe('DataTypes', () => { await testSimpleInOutRaw(vars.User, 'booleanAttr', true, 1); await testSimpleInOutRaw(vars.User, 'booleanAttr', false, 0); }); + } else if (dialect.name === 'oracle') { + // Oracle uses CHAR(1). + it('is deserialized as a char string when DataType is not specified', async () => { + await testSimpleInOutRaw(vars.User, 'booleanAttr', true, '1'); + await testSimpleInOutRaw(vars.User, 'booleanAttr', false, '0'); + }); } else { it('is deserialized as a boolean when DataType is not specified', async () => { await testSimpleInOutRaw(vars.User, 'booleanAttr', true, true); @@ -666,6 +687,7 @@ describe('DataTypes', () => { it('accepts numbers, bigints, strings', async () => { await testSimpleInOut(vars.User, 'intAttr', 123, 123); await testSimpleInOut(vars.User, 'intAttr', 123n, 123); + await testSimpleInOut(vars.User, 'intAttr', '123', 123); await testSimpleInOut( @@ -731,7 +753,6 @@ describe('DataTypes', () => { it('rejects unsafe integers', async () => { await expect(vars.User.create({ bigintAttr: 9_007_199_254_740_992 })).to.be.rejected; await expect(vars.User.create({ bigintAttr: -9_007_199_254_740_992 })).to.be.rejected; - await expect(vars.User.create({ bigintAttr: 123.4 })).to.be.rejected; await expect(vars.User.create({ bigintAttr: Number.NaN })).to.be.rejected; await expect(vars.User.create({ bigintAttr: Number.NEGATIVE_INFINITY })).to.be.rejected; @@ -745,7 +766,10 @@ describe('DataTypes', () => { }); it('is deserialized as a string when DataType is not specified', async () => { - await testSimpleInOutRaw(vars.User, 'bigintAttr', 123n, '123'); + // Oracle inferences the datatype from the metadata and return the values in respective format. + if (dialect.name !== 'oracle') { + await testSimpleInOutRaw(vars.User, 'bigintAttr', 123n, '123'); + } }); if (dialect.supports.dataTypes.INTS.unsigned) { @@ -980,9 +1004,10 @@ describe('DataTypes', () => { await expect(vars.User.create({ decimalAttr: 'abc' })).to.be.rejected; }); - if (dialect.name === 'sqlite3') { + if (dialect.name === 'sqlite3' || dialect.name === 'oracle') { // sqlite3 doesn't give us a way to do sql type-based parsing, *and* returns bigints as js numbers. // this behavior is undesired but is still tested against to ensure we update this test when this is finally fixed. + // Oracle inferences the datatype from the metadata and return the values in respective format. it('is deserialized as a number when DataType is not specified (undesired sqlite limitation)', async () => { await testSimpleInOutRaw(vars.User, 'decimalAttr', 123n, 123); }); @@ -1033,6 +1058,7 @@ describe('DataTypes', () => { 123n, dialect.name === 'mssql' ? '123' : '123.00', ); + await testSimpleInOut( vars.User, 'decimalAttr', @@ -1195,7 +1221,9 @@ describe('DataTypes', () => { ? '2022-01-01 00:00:00.000 +00:00' : dialect.name === 'db2' ? '2022-01-01 00:00:00.000000+00' - : '2022-01-01 00:00:00+00', + : dialect.name === 'oracle' + ? new Date('2022-01-01T00:00:00Z') + : '2022-01-01 00:00:00+00', ); }); }); @@ -1233,7 +1261,7 @@ describe('DataTypes', () => { it('clamps to specified precision', async () => { // sqlite does not support restricting the precision - if (dialect.name !== 'sqlite3') { + if (dialect.name !== 'sqlite3' && dialect.name !== 'oracle') { await testSimpleInOut( vars.User, 'dateMinPrecisionAttr', @@ -1333,9 +1361,20 @@ describe('DataTypes', () => { } }); - it(`is deserialized as a string when DataType is not specified`, async () => { - await testSimpleInOutRaw(vars.User, 'dateAttr', '2022-01-01', '2022-01-01'); - }); + if (dialect.name === 'oracle') { + it(`is deserialized as a date when DataType is not specified`, async () => { + await testSimpleInOutRaw( + vars.User, + 'dateAttr', + '2022-01-01', + new Date('2022-01-01T00:00:00.000Z'), + ); + }); + } else { + it(`is deserialized as a string when DataType is not specified`, async () => { + await testSimpleInOutRaw(vars.User, 'dateAttr', '2022-01-01', '2022-01-01'); + }); + } }); describe('TIME(precision)', () => { @@ -1705,40 +1744,48 @@ describe('DataTypes', () => { // TODO: expected for mariadb 10.4 : https://jira.mariadb.org/browse/MDEV-15558 expect(table.jsonStr.type).to.equal('LONGTEXT'); break; + case 'oracle': + expect(table.jsonStr.type).to.equal('BLOB'); + break; default: expect(table.jsonStr.type).to.equal(jsonTypeName); } }); - it('properly serializes default values', async () => { - const createdUser = await vars.User.create(); - await createdUser.reload(); - expect(createdUser.get()).to.deep.eq({ - jsonStr: 'abc', - jsonBoolean: true, - jsonNumber: 1, - jsonNull: null, - jsonArray: ['a', 'b'], - jsonObject: { key: 'abc' }, - id: 1, + // Oracle Database < 21 doesn't consider scalars as JSON column + // thus, fails the CHECK constraint test. + if (dialect.name !== 'oracle') { + it('properly serializes default values', async () => { + const createdUser = await vars.User.create(); + await createdUser.reload(); + expect(createdUser.get()).to.deep.eq({ + jsonStr: 'abc', + jsonBoolean: true, + jsonNumber: 1, + jsonNull: null, + jsonArray: ['a', 'b'], + jsonObject: { key: 'abc' }, + id: 1, + }); }); - }); - it('properly serializes values', async () => { - await testSimpleInOut(vars.User, 'jsonStr', 'abc', 'abc'); - await testSimpleInOut(vars.User, 'jsonBoolean', true, true); - await testSimpleInOut(vars.User, 'jsonBoolean', false, false); - await testSimpleInOut(vars.User, 'jsonNumber', 123.4, 123.4); - await testSimpleInOut(vars.User, 'jsonArray', [1, 2], [1, 2]); - await testSimpleInOut(vars.User, 'jsonObject', { a: 1 }, { a: 1 }); - await testSimpleInOut(vars.User, 'jsonNull', null, null); - }); + it('properly serializes values', async () => { + await testSimpleInOut(vars.User, 'jsonStr', 'abc', 'abc'); + await testSimpleInOut(vars.User, 'jsonBoolean', true, true); + await testSimpleInOut(vars.User, 'jsonBoolean', false, false); + await testSimpleInOut(vars.User, 'jsonNumber', 123.4, 123.4); + await testSimpleInOut(vars.User, 'jsonArray', [1, 2], [1, 2]); + await testSimpleInOut(vars.User, 'jsonObject', { a: 1 }, { a: 1 }); + await testSimpleInOut(vars.User, 'jsonNull', null, null); + }); + } // MariaDB: supports a JSON type, but: // - MariaDB 10.5 says it's a JSON col, on which we enabled automatic JSON parsing. // - MariaDB 10.4 says it's a string, so we can't parse it based on the type. + // Oracle JSON is BLOB column with check `IS JSON`. // TODO [2024-06-18]: Re-enable this test when we drop support for MariaDB < 10.5 - if (dialect.name !== 'mariadb') { + if (dialect.name !== 'mariadb' && dialect.name !== 'oracle') { if (dialect.name === 'mssql' || dialect.name === 'sqlite3') { // MSSQL: does not have a JSON type, so we can't parse it if our DataType is not specified. // SQLite: sqlite3 does not tell us the type of a column, we cannot parse based on it. diff --git a/packages/core/test/integration/data-types/methods.test.ts b/packages/core/test/integration/data-types/methods.test.ts index 7c2320c19f13..c96a0c848e33 100644 --- a/packages/core/test/integration/data-types/methods.test.ts +++ b/packages/core/test/integration/data-types/methods.test.ts @@ -16,6 +16,13 @@ import { beforeAll2, beforeEach2, sequelize, setResetMode } from '../support'; const dialect = sequelize.dialect; describe('DataType Methods', () => { + // For a custom data-type definition for Oracle, _getBindDef() is required to + // provide information about BINDOUT variables. Similar tests have been added + // in dialects/oracle/data-types/methods.test.ts + if (dialect.name === 'oracle') { + return; + } + setResetMode('none'); const customValueSymbol = Symbol('dummy'); diff --git a/packages/core/test/integration/dialects/oracle/data-types/methods.test.ts b/packages/core/test/integration/dialects/oracle/data-types/methods.test.ts new file mode 100644 index 000000000000..29409f98fea8 --- /dev/null +++ b/packages/core/test/integration/dialects/oracle/data-types/methods.test.ts @@ -0,0 +1,213 @@ +import type { + BelongsToManyAddAssociationMixin, + CreationOptional, + ForeignKey, + InferAttributes, + InferCreationAttributes, + NonAttribute, +} from '@sequelize/core'; +import { DataTypes, Model } from '@sequelize/core'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { beforeAll2, beforeEach2, sequelize, setResetMode } from '../../../support'; + +// This test suite ensures DataType methods are called at the appropriate time + +const dialect = sequelize.dialect; + +describe('DataType Methods', () => { + setResetMode('none'); + + const customValueSymbol = Symbol('dummy'); + + class CustomDataType extends DataTypes.STRING { + parseDatabaseValue(_value: unknown): any { + return customValueSymbol; + } + + _getBindDef(oracledb: any) { + return { type: oracledb.DB_TYPE_VARCHAR, maxSize: 255 }; + } + } + + const models = beforeAll2(async () => { + class User extends Model, InferCreationAttributes> { + declare id: CreationOptional; + declare name: string | null; + declare projects?: NonAttribute; + } + + User.init( + { + id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, + name: { type: CustomDataType, allowNull: true, field: 'first_name' }, + }, + { sequelize }, + ); + + class Project extends Model, InferCreationAttributes> { + declare name: string; + declare userId: ForeignKey; + declare stakeholders?: NonAttribute>; + + declare addStakeholder: BelongsToManyAddAssociationMixin; + } + + Project.init( + { + name: { type: CustomDataType, allowNull: false }, + }, + { sequelize }, + ); + + class ProjectStakeholder extends Model< + InferAttributes, + InferCreationAttributes + > { + declare key: string; + } + + ProjectStakeholder.init( + { + key: { type: CustomDataType, allowNull: false }, + }, + { sequelize, noPrimaryKey: true, timestamps: false }, + ); + + Project.belongsTo(User, { + as: 'user', + inverse: { as: 'projects', type: 'hasMany' }, + foreignKey: 'userId', + }); + + Project.belongsToMany(User, { + as: 'stakeholders', + inverse: 'stakeholdings', + through: ProjectStakeholder, + }); + + await User.sync({ force: true }); + await Project.sync({ force: true }); + await ProjectStakeholder.sync({ force: true }); + + const user1 = await User.create({ name: 'John' }); + const user2 = await User.create({ name: 'Stakeholder User' }); + const project = await Project.create({ name: 'Project 1', userId: user1.id }); + await project.addStakeholder(user2, { through: { key: 'dummy-value' } }); + + return { User, Project, ProjectStakeholder }; + }); + + const spies = beforeEach2(() => { + // add mocha spy to sanitize + return { + sanitize: sinon.spy(DataTypes.STRING.prototype, 'sanitize'), + validate: sinon.spy(DataTypes.STRING.prototype, 'validate'), + parseDatabaseValue: sinon.spy(CustomDataType.prototype, 'parseDatabaseValue'), + }; + }); + + afterEach(() => { + for (const spy of Object.values(spies)) { + spy.restore(); + } + }); + + it(`setting a value on a model only calls 'sanitize'`, () => { + models.User.build({ name: 'foo' }); + + expect(spies.sanitize.calledOnce).to.eq(true, 'sanitized not called exactly once'); + expect(spies.validate.called).to.eq(false, 'validate should not have been called'); + expect(spies.parseDatabaseValue.called).to.eq( + false, + 'parseDatabaseValue should not have been called', + ); + }); + + it(`retrieving a model only calls 'parseDatabaseValue' (no join)`, async () => { + const out = await models.User.findOne({ rejectOnEmpty: true }); + + expect(out.name).to.eq(customValueSymbol, 'parseDatabaseValue not called on top level model'); + + expect(spies.sanitize.called).to.eq(false, 'sanitize should not have been called'); + expect(spies.validate.called).to.eq(false, 'validate should not have been called'); + }); + + it(`retrieving a model only calls 'parseDatabaseValue' (with join)`, async () => { + // this test is separate from the no-join version because they use different code paths. + // We test both double nested associations & that through tables are handled correctly. + const out = await models.User.findOne({ + include: [ + { + association: 'projects', + include: ['stakeholders'], + }, + ], + rejectOnEmpty: true, + }); + + expect(out.name).to.eq(customValueSymbol, 'parseDatabaseValue not called on top level model'); + expect(out.projects![0].name).to.eq( + customValueSymbol, + 'parseDatabaseValue not called on first include level', + ); + expect(out.projects![0].stakeholders![0].name).to.eq( + customValueSymbol, + 'parseDatabaseValue not called on second include level', + ); + expect(out.projects![0].stakeholders![0].ProjectStakeholder.key).to.eq( + customValueSymbol, + 'parseDatabaseValue not called on Many-To-Many through table', + ); + + expect(spies.sanitize.called).to.eq(false, 'sanitize should not have been called'); + expect(spies.validate.called).to.eq(false, 'validate should not have been called'); + }); + + if (dialect.supports.returnValues) { + it(`inserting a model calls 'parseDatabaseValue' on returned values`, async () => { + // 'name' attr has a different name in the database + const out = await models.User.create({ name: 'foo' }, { returning: true }); + + expect(out.name).to.eq(customValueSymbol, 'parseDatabaseValue has not been called'); + + // sanitize is called when the user input is added to the model + expect(spies.sanitize.called).to.eq(true, 'sanitize should have been called'); + // validate is called before persisting the model + expect(spies.validate.called).to.eq(true, 'validate should have been called'); + }); + + it(`upserting a model calls 'parseDatabaseValue' on returned values`, async () => { + // 'name' attr has a different name in the database + const [out] = await models.User.upsert({ name: 'foo', id: 1234 }, { returning: true }); + + expect(out.name).to.eq(customValueSymbol, 'parseDatabaseValue has not been called'); + + // sanitize is called when the user input is added to the model + expect(spies.sanitize.called).to.eq(true, 'sanitize should have been called'); + // validate is called before persisting the model + expect(spies.validate.called).to.eq(true, 'validate should have been called'); + }); + } + + if (dialect.supports.returnValues === 'returning') { + it(`updating a model calls 'parseDatabaseValue' on returned values`, async () => { + const user = await models.User.create({ name: 'foo' }); + user.name = 'bob'; + await user.save({ returning: true }); + + expect(user.name).to.eq(customValueSymbol, 'parseDatabaseValue has not been called'); + }); + } + + it(`does not call 'parseDatabaseValue' on null values`, async () => { + const user = await models.User.create({ name: null }); + await user.reload(); + + expect(user.name).to.eq(null, 'parseDatabaseValue called on null value'); + expect(spies.parseDatabaseValue.called).to.eq( + false, + 'parseDatabaseValue should not have been called', + ); + }); +}); diff --git a/packages/core/test/integration/error.test.ts b/packages/core/test/integration/error.test.ts index e7784ddb56ae..c38e2d0ca264 100644 --- a/packages/core/test/integration/error.test.ts +++ b/packages/core/test/integration/error.test.ts @@ -492,7 +492,7 @@ describe(getTestDialectTeaser('Sequelize Errors'), () => { await expect(User.create({ name: 'jan' })).to.be.rejectedWith(UniqueConstraintError); // And when the model is not passed at all - if (['db2', 'ibmi'].includes(dialect)) { + if (['db2', 'ibmi', 'oracle'].includes(dialect)) { await expect( sequelize.query('INSERT INTO "users" ("name") VALUES (\'jan\')'), ).to.be.rejectedWith(UniqueConstraintError); @@ -550,7 +550,7 @@ describe(getTestDialectTeaser('Sequelize Errors'), () => { } catch (error) { expect(error).to.be.instanceOf(ValidationError); assert(error instanceof ValidationError); - if (dialect === 'db2') { + if (dialect === 'db2' || dialect === 'oracle') { expect(error.errors).to.have.length(0); } else { expect(error.errors).to.have.length(1); @@ -603,6 +603,10 @@ describe(getTestDialectTeaser('Sequelize Errors'), () => { expect(error.errors[0].message).to.equal('username must be unique'); break; + case 'oracle': + expect(error.cause.message).to.match(/ORA-00001: unique constraint \(.*\) violated/); + break; + default: expect(error.cause.message).to.contain("Duplicate entry 'foo' for key 'username'"); expect(error.errors[0].path).to.equal('username'); @@ -626,7 +630,7 @@ describe(getTestDialectTeaser('Sequelize Errors'), () => { } catch (error) { expect(error).to.be.instanceOf(ValidationError); assert(error instanceof ValidationError); - if (dialect === 'db2') { + if (dialect === 'db2' || dialect === 'oracle') { expect(error.errors).to.have.length(0); } else { expect(error.errors).to.have.length(1); @@ -675,6 +679,12 @@ describe(getTestDialectTeaser('Sequelize Errors'), () => { ); break; + case 'oracle': + expect(error.cause.message).to.match( + /ORA-00001: unique constraint \(.*.users_username_unique\) violated/, + ); + break; + default: expect(error.cause.message).to.contain( "Duplicate entry 'foo' for key 'users_username_unique'", @@ -711,7 +721,7 @@ describe(getTestDialectTeaser('Sequelize Errors'), () => { } catch (error) { expect(error).to.be.instanceOf(ForeignKeyConstraintError); assert(error instanceof ForeignKeyConstraintError); - if (dialect === 'sqlite3') { + if (dialect === 'sqlite3' || dialect === 'oracle') { expect(error.index).to.be.undefined; } else { expect(error.index).to.equal('Tasks_userId_Users_fk'); @@ -750,6 +760,14 @@ describe(getTestDialectTeaser('Sequelize Errors'), () => { ); break; + case 'oracle': + expect(error.table).to.be.undefined; + expect(error.fields).to.be.null; + expect(error.cause.message).to.match( + /ORA-02292: integrity constraint \(.*.Tasks_userId_Users_fk\) violated - child record found/, + ); + break; + default: expect(error.table).to.equal('Users'); expect(error.fields).to.deep.equal(['userId']); @@ -786,7 +804,7 @@ describe(getTestDialectTeaser('Sequelize Errors'), () => { } catch (error) { expect(error).to.be.instanceOf(ForeignKeyConstraintError); assert(error instanceof ForeignKeyConstraintError); - if (dialect === 'sqlite3') { + if (dialect === 'sqlite3' || dialect === 'oracle') { expect(error.index).to.be.undefined; } else { expect(error.index).to.equal('Tasks_userId_Users_fk'); @@ -825,6 +843,14 @@ describe(getTestDialectTeaser('Sequelize Errors'), () => { ); break; + case 'oracle': + expect(error.table).to.be.undefined; + expect(error.fields).to.be.null; + expect(error.cause.message).to.match( + /ORA-02291: integrity constraint \(.*.Tasks_userId_Users_fk\) violated - parent key not found/, + ); + break; + default: expect(error.table).to.equal('Users'); expect(error.fields).to.deep.equal(['userId']); @@ -867,6 +893,10 @@ describe(getTestDialectTeaser('Sequelize Errors'), () => { assert(error.errors[2] instanceof UnknownConstraintError); expect(error.errors[2].constraint).to.equal('unique_constraint'); expect(error.errors[2].table).to.equal('Users'); + } else if (dialect === 'oracle') { + expect(error).to.be.instanceOf(DatabaseError); + assert(error instanceof DatabaseError); + expect(error.message).to.match(/^ORA-02264: name already used by an existing constraint/); } else { expect(error).to.be.instanceOf(DatabaseError); assert(error instanceof DatabaseError); diff --git a/packages/core/test/integration/include.test.js b/packages/core/test/integration/include.test.js index 842b211aac45..59c7b499f14e 100644 --- a/packages/core/test/integration/include.test.js +++ b/packages/core/test/integration/include.test.js @@ -753,6 +753,20 @@ Instead of specifying a Model, either: break; } + case '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', + ], + ]; + + break; + } + default: { findAttributes = [ Sequelize.literal('EXISTS(SELECT 1) AS "postComments.someProperty"'), diff --git a/packages/core/test/integration/include/findAll.test.js b/packages/core/test/integration/include/findAll.test.js index d27bfee27d45..5d39f0fbe4a8 100644 --- a/packages/core/test/integration/include/findAll.test.js +++ b/packages/core/test/integration/include/findAll.test.js @@ -121,7 +121,7 @@ describe(Support.getTestDialectTeaser('Include'), () => { { title: 'Pen' }, { title: 'Monitor' }, ]); - const products = await Product.findAll(); + const products = await Product.findAll({ order: [['id', 'ASC']] }); const groupMembers = [ { groupId: groups[0].id, rankId: ranks[0].id }, { groupId: groups[1].id, rankId: ranks[2].id }, @@ -343,7 +343,9 @@ describe(Support.getTestDialectTeaser('Include'), () => { for (const i of [0, 1, 2, 3, 4]) { const [user, products] = await Promise.all([ User.create(), - Product.bulkCreate([{ title: 'Chair' }, { title: 'Desk' }]).then(() => Product.findAll()), + Product.bulkCreate([{ title: 'Chair' }, { title: 'Desk' }]).then(() => + Product.findAll({ order: [['id', 'ASC']] }), + ), ]); await Promise.all([ GroupMember.bulkCreate([ @@ -1174,7 +1176,7 @@ describe(Support.getTestDialectTeaser('Include'), () => { await Product.bulkCreate([{ title: 'Chair' }, { 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 }, diff --git a/packages/core/test/integration/include/schema.test.js b/packages/core/test/integration/include/schema.test.js index c19b4335af3d..a014d20e0596 100644 --- a/packages/core/test/integration/include/schema.test.js +++ b/packages/core/test/integration/include/schema.test.js @@ -154,7 +154,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 }, @@ -273,7 +273,9 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { for (const i of [0, 1, 2, 3, 4]) { const [user, products] = await Promise.all([ AccUser.create(), - Product.bulkCreate([{ title: 'Chair' }, { title: 'Desk' }]).then(() => Product.findAll()), + Product.bulkCreate([{ title: 'Chair' }, { title: 'Desk' }]).then(() => + Product.findAll({ order: [['id', 'ASC']] }), + ), ]); await Promise.all([ GroupMember.bulkCreate([ @@ -954,7 +956,9 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), () => { for (const i of [0, 1, 2, 3, 4]) { const [user, products] = await Promise.all([ User.create({ name: 'FooBarzz' }), - Product.bulkCreate([{ title: 'Chair' }, { title: 'Desk' }]).then(() => Product.findAll()), + Product.bulkCreate([{ title: 'Chair' }, { title: 'Desk' }]).then(() => + Product.findAll({ order: [['id', 'ASC']] }), + ), ]); await Promise.all([ GroupMember.bulkCreate([ diff --git a/packages/core/test/integration/instance/values.test.js b/packages/core/test/integration/instance/values.test.js index e79930e0d244..1f089eb96217 100644 --- a/packages/core/test/integration/instance/values.test.js +++ b/packages/core/test/integration/instance/values.test.js @@ -111,7 +111,9 @@ describe(Support.getTestDialectTeaser('DAO'), () => { ? this.sequelize.fn('', this.sequelize.fn('datetime', 'now')) : dialect === 'mssql' ? this.sequelize.fn('', this.sequelize.fn('getdate')) - : this.sequelize.fn('NOW'); + : dialect === 'oracle' + ? this.sequelize.fn('', this.sequelize.literal('SYSDATE')) + : this.sequelize.fn('NOW'); user.set({ d: now, diff --git a/packages/core/test/integration/json.test.ts b/packages/core/test/integration/json.test.ts index f59f3034726d..392dcc49ab6a 100644 --- a/packages/core/test/integration/json.test.ts +++ b/packages/core/test/integration/json.test.ts @@ -58,6 +58,10 @@ describe('JSON Manipulation', () => { }); it('should be able to store strings that require escaping', async () => { + if (dialect.name === 'oracle') { + return; + } + const text = 'Multi-line \n \'$string\' needing "escaping" for $$ and $1 type values'; await vars.User.create({ jsonAttr: text }); @@ -69,8 +73,10 @@ describe('JSON Manipulation', () => { const JSON_OBJECT = { name: 'swen', phones: [1337, 42] }; const JSON_STRING = 'kate'; +// Oracle database < 21 doesn't supports scalars to be treated as JSON +// thus fails with CHECk constraint violation errors describe('JSON Querying', () => { - if (!dialect.supports.dataTypes.JSON) { + if (!dialect.supports.dataTypes.JSON || dialect.name === 'oracle') { return; } diff --git a/packages/core/test/integration/model-repository/bulk-destroy.test.ts b/packages/core/test/integration/model-repository/bulk-destroy.test.ts index fd97cb31e01f..ba878c0f1953 100644 --- a/packages/core/test/integration/model-repository/bulk-destroy.test.ts +++ b/packages/core/test/integration/model-repository/bulk-destroy.test.ts @@ -116,6 +116,16 @@ describe('ModelRepository#_UNSTABLE_bulkDestroy', () => { ], { genericQuotes: true }, ), + oracle: toMatchSql([ + 'BEGIN TRANSACTION', + 'SELECT "id", "createdAt", "updatedAt" FROM "Users" "User" WHERE "User"."id" = 1;', + 'SELECT "id", "ownerId", "createdAt", "updatedAt" FROM "Projects" "Project" WHERE "Project"."ownerId" IN (1);', + 'SELECT "id", "projectId", "createdAt", "updatedAt" FROM "Tasks" "Task" WHERE "Task"."projectId" IN (1);', + 'DELETE FROM "Tasks" WHERE "id" = 1', + 'DELETE FROM "Projects" WHERE "id" = 1', + 'DELETE FROM "Users" WHERE "id" = 1', + 'COMMIT TRANSACTION', + ]), }); }); @@ -155,6 +165,14 @@ describe('ModelRepository#_UNSTABLE_bulkDestroy', () => { 'DELETE FROM [Projects] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;', 'DELETE FROM [Users] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;', ]), + oracle: toMatchSql([ + 'SELECT "id", "createdAt", "updatedAt" FROM "Users" "User" WHERE "User"."id" = 1;', + 'SELECT "id", "ownerId", "createdAt", "updatedAt" FROM "Projects" "Project" WHERE "Project"."ownerId" IN (1);', + 'SELECT "id", "projectId", "createdAt", "updatedAt" FROM "Tasks" "Task" WHERE "Task"."projectId" IN (1);', + 'DELETE FROM "Tasks" WHERE "id" = 1', + 'DELETE FROM "Projects" WHERE "id" = 1', + 'DELETE FROM "Users" WHERE "id" = 1', + ]), }); }); }); diff --git a/packages/core/test/integration/model-repository/destroy.test.ts b/packages/core/test/integration/model-repository/destroy.test.ts index c800557bb72d..885c0cf46144 100644 --- a/packages/core/test/integration/model-repository/destroy.test.ts +++ b/packages/core/test/integration/model-repository/destroy.test.ts @@ -109,6 +109,15 @@ describe('ModelRepository#_UNSTABLE_destroy', () => { ], { genericQuotes: true }, ), + oracle: toMatchSql([ + 'BEGIN TRANSACTION', + 'SELECT "id", "ownerId", "createdAt", "updatedAt" FROM "Projects" "Project" WHERE "Project"."ownerId" IN (1);', + 'SELECT "id", "projectId", "createdAt", "updatedAt" FROM "Tasks" "Task" WHERE "Task"."projectId" IN (1);', + 'DELETE FROM "Tasks" WHERE "id" = 1', + 'DELETE FROM "Projects" WHERE "id" = 1', + 'DELETE FROM "Users" WHERE "id" = 1', + 'COMMIT TRANSACTION', + ]), }); }); @@ -144,6 +153,13 @@ describe('ModelRepository#_UNSTABLE_destroy', () => { 'DELETE FROM [Projects] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;', 'DELETE FROM [Users] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;', ]), + oracle: toMatchSql([ + 'SELECT "id", "ownerId", "createdAt", "updatedAt" FROM "Projects" "Project" WHERE "Project"."ownerId" IN (1);', + 'SELECT "id", "projectId", "createdAt", "updatedAt" FROM "Tasks" "Task" WHERE "Task"."projectId" IN (1);', + 'DELETE FROM "Tasks" WHERE "id" = 1', + 'DELETE FROM "Projects" WHERE "id" = 1', + 'DELETE FROM "Users" WHERE "id" = 1', + ]), }); }); }); diff --git a/packages/core/test/integration/model.test.js b/packages/core/test/integration/model.test.js index 6fee10188e18..2f7d58de63a6 100644 --- a/packages/core/test/integration/model.test.js +++ b/packages/core/test/integration/model.test.js @@ -286,6 +286,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { } case 'db2': + case 'oracle': case 'mssql': { expect(index.fields).to.deep.equal([ { attribute: 'user_name', collate: undefined, length: undefined, order: 'ASC' }, @@ -551,6 +552,28 @@ describe(Support.getTestDialectTeaser('Model'), () => { break; } + case '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 }, + ]); + + break; + } + case 'db2': { idx1 = args[1]; @@ -1012,7 +1035,9 @@ describe(Support.getTestDialectTeaser('Model'), () => { if (dialectName === 'sqlite3' && sql.includes('TABLE_INFO')) { test++; expect(sql).to.not.contain('special'); - } else if (['mysql', 'mssql', 'mariadb', 'db2', 'ibmi'].includes(dialectName)) { + } else if ( + ['mysql', 'mssql', 'mariadb', 'db2', 'ibmi', 'oracle'].includes(dialectName) + ) { test++; expect(sql).to.not.contain('special'); } @@ -1031,7 +1056,9 @@ describe(Support.getTestDialectTeaser('Model'), () => { if (dialectName === 'sqlite3' && sql.includes('TABLE_INFO')) { test++; expect(sql).to.contain('special'); - } else if (['mysql', 'mssql', 'mariadb', 'db2', 'ibmi'].includes(dialectName)) { + } else if ( + ['mysql', 'mssql', 'mariadb', 'db2', 'ibmi', 'oracle'].includes(dialectName) + ) { test++; expect(sql).to.contain('special'); } @@ -1080,7 +1107,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { switch (dialectName) { case 'postgres': case 'db2': - case 'ibmi': { + case 'ibmi': + case 'oracle': { expect(sql).to.match(/REFERENCES\s+"prefix"\."UserPubs" \("id"\)/); break; @@ -1127,7 +1155,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { switch (dialectName) { case 'postgres': case 'db2': - case 'ibmi': { + case 'ibmi': + case 'oracle': { expect(UserPublic).to.include('INSERT INTO "UserPublics"'); break; @@ -1159,7 +1188,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { switch (dialectName) { case 'postgres': case 'db2': - case 'ibmi': { + case 'ibmi': + case 'oracle': { expect(UserSpecial).to.include('INSERT INTO "special"."UserSpecials"'); break; @@ -1197,7 +1227,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { switch (dialectName) { case 'postgres': case 'db2': - case 'ibmi': { + case 'ibmi': + case 'oracle': { expect(user).to.include('UPDATE "special"."UserSpecials"'); break; @@ -1395,6 +1426,12 @@ describe(Support.getTestDialectTeaser('Model'), () => { break; } + case 'oracle': { + expect(error.message).to.match(/^ORA-00942:/); + + break; + } + case 'ibmi': { expect(error.message).to.match( /[a-zA-Z0-9[\] /-]+?"4uth0r5" in SEQUELIZE type \*FILE not found\./, diff --git a/packages/core/test/integration/model/attributes/field.test.js b/packages/core/test/integration/model/attributes/field.test.js index 821f61d4b453..39d680855e81 100644 --- a/packages/core/test/integration/model/attributes/field.test.js +++ b/packages/core/test/integration/model/attributes/field.test.js @@ -487,6 +487,16 @@ 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/packages/core/test/integration/model/attributes/types.test.js b/packages/core/test/integration/model/attributes/types.test.js index 91c445c95e58..1b041a32a6f5 100644 --- a/packages/core/test/integration/model/attributes/types.test.js +++ b/packages/core/test/integration/model/attributes/types.test.js @@ -112,6 +112,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { 'CAST(CASE WHEN EXISTS(SELECT 1) THEN 1 ELSE 0 END AS BIT) AS "someBoolean"'; } else if (['db2', 'ibmi'].includes(dialect)) { 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({ diff --git a/packages/core/test/integration/model/bulk-create.test.js b/packages/core/test/integration/model/bulk-create.test.js index d585e6fcc536..b2708e970050 100644 --- a/packages/core/test/integration/model/bulk-create.test.js +++ b/packages/core/test/integration/model/bulk-create.test.js @@ -170,6 +170,7 @@ describe('Model', () => { logging(sql) { switch (dialectName) { case 'postgres': + case 'oracle': case 'ibmi': { expect(sql).to.include( 'INSERT INTO "Beers" ("id","style","createdAt","updatedAt") VALUES (DEFAULT', diff --git a/packages/core/test/integration/model/count.test.js b/packages/core/test/integration/model/count.test.js index 5fdfb23d50e5..0c3ca60920eb 100644 --- a/packages/core/test/integration/model/count.test.js +++ b/packages/core/test/integration/model/count.test.js @@ -55,7 +55,12 @@ describe('Model.count', () => { }); }); - if (dialectName !== 'mssql' && dialectName !== 'db2' && dialectName !== 'ibmi') { + if ( + dialectName !== 'mssql' && + dialectName !== 'db2' && + dialectName !== 'ibmi' && + dialectName !== 'oracle' + ) { describe('aggregate', () => { it('allows grouping by aliased attribute', async function () { await this.User.aggregate('id', 'count', { diff --git a/packages/core/test/integration/model/create.test.js b/packages/core/test/integration/model/create.test.js index fcf096815ca1..b6bedb83006e 100644 --- a/packages/core/test/integration/model/create.test.js +++ b/packages/core/test/integration/model/create.test.js @@ -495,7 +495,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { } it('should not fail silently with concurrency higher than pool, a unique constraint and a create hook resulting in mismatched values', async function () { - if (['sqlite3', 'mssql', 'db2', 'ibmi'].includes(dialectName)) { + if (['sqlite3', 'mssql', 'db2', 'ibmi', 'oracle'].includes(dialectName)) { return; } @@ -911,7 +911,9 @@ describe(Support.getTestDialectTeaser('Model'), () => { ? '$sequelize_1' : dialectName === 'mssql' ? '@sequelize_1' - : '?'; + : dialectName === 'oracle' + ? ':1' + : '?'; let match = false; const user = await this.User.create( diff --git a/packages/core/test/integration/model/findAll.test.js b/packages/core/test/integration/model/findAll.test.js index d8c8839467af..356a277137f8 100644 --- a/packages/core/test/integration/model/findAll.test.js +++ b/packages/core/test/integration/model/findAll.test.js @@ -26,7 +26,9 @@ describe(Support.getTestDialectTeaser('Model'), () => { intVal: DataTypes.INTEGER, theDate: DataTypes.DATE, aBool: DataTypes.BOOLEAN, - binary: DataTypes.BLOB, + ...(dialectName === 'oracle' + ? { binary: DataTypes.STRING(16, true) } + : { binary: DataTypes.BLOB }), }); await this.User.sync({ force: true }); diff --git a/packages/core/test/integration/model/findAll/order.test.js b/packages/core/test/integration/model/findAll/order.test.js index ca52996eca7e..c70a23bdb913 100644 --- a/packages/core/test/integration/model/findAll/order.test.js +++ b/packages/core/test/integration/model/findAll/order.test.js @@ -24,7 +24,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { }); }); - if (current.dialect.name !== 'mssql' && current.dialect.name !== 'ibmi') { + if (!['oracle', 'ibmi', 'mssql'].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({ @@ -96,7 +96,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { } it('should not throw on a literal', async function () { - if (['db2', 'ibmi'].includes(current.dialect.name)) { + if (['db2', 'ibmi', 'oracle'].includes(current.dialect.name)) { await this.User.findAll({ order: [['id', this.sequelize.literal('ASC, "name" DESC')]], }); diff --git a/packages/core/test/integration/model/json.test.js b/packages/core/test/integration/model/json.test.js index d0cc7050f802..5ca355e8f1b8 100644 --- a/packages/core/test/integration/model/json.test.js +++ b/packages/core/test/integration/model/json.test.js @@ -286,6 +286,10 @@ describe(Support.getTestDialectTeaser('Model'), () => { if (dialect.supports.jsonOperations && dialect.supports.jsonExtraction.quoted) { it('should query an instance with JSONB data and order while trying to inject', async function () { + if (dialect.name === 'oracle') { + return; + } + await this.Event.create({ data: { name: { diff --git a/packages/core/test/integration/model/paranoid.test.js b/packages/core/test/integration/model/paranoid.test.js index b8070ba29f77..36abe38ed471 100644 --- a/packages/core/test/integration/model/paranoid.test.js +++ b/packages/core/test/integration/model/paranoid.test.js @@ -125,6 +125,11 @@ describe('Paranoid Model', () => { }); it('should soft delete with JSON condition', async function () { + // Oracle stores JSON as BLOB. where condition with equality isn't supported for this. + if (dialect.name === 'oracle') { + return; + } + await this.Model.bulkCreate([ { name: 'One', diff --git a/packages/core/test/integration/model/update.test.ts b/packages/core/test/integration/model/update.test.ts index 123dbe0c6279..0233e139aace 100644 --- a/packages/core/test/integration/model/update.test.ts +++ b/packages/core/test/integration/model/update.test.ts @@ -459,6 +459,7 @@ describe('Model.update', () => { mssql: `UPDATE [users1] SET [secretValue]=@sequelize_1,[updatedAt]=@sequelize_2 OUTPUT INSERTED.* WHERE [id] = @sequelize_3`, db2: `SELECT * FROM FINAL TABLE (UPDATE "users1" SET "secretValue"=?,"updatedAt"=? WHERE "id" = ?);`, ibmi: `UPDATE "users1" SET "secretValue"=?,"updatedAt"=? WHERE "id" = ?;`, + oracle: `UPDATE "users1" SET "secretValue"=:1,"updatedAt"=:2 WHERE "id" = :3`, }); }, returning: [sql.col('*')], diff --git a/packages/core/test/integration/pool.test.ts b/packages/core/test/integration/pool.test.ts index 58894933e8d1..1ca432f6fdf3 100644 --- a/packages/core/test/integration/pool.test.ts +++ b/packages/core/test/integration/pool.test.ts @@ -41,6 +41,10 @@ function assertSameConnection( expect(newConnection.dummyId).to.equal(oldConnection.dummyId).and.to.be.ok; break; + case 'oracle': + expect(oldConnection).to.be.equal(newConnection).and.to.be.ok; + break; + default: throw new Error('Unsupported dialect'); } @@ -75,6 +79,10 @@ function assertNewConnection(newConnection: AbstractConnection, oldConnection: A expect(oldConnection.dummyId).to.be.ok; break; + case 'oracle': + expect(oldConnection).to.not.be.equal(newConnection); + break; + default: throw new Error('Unsupported dialect'); } diff --git a/packages/core/test/integration/query-interface/add-show-remove-constraint.test.ts b/packages/core/test/integration/query-interface/add-show-remove-constraint.test.ts index 4668d2143bae..bf1b187d53c8 100644 --- a/packages/core/test/integration/query-interface/add-show-remove-constraint.test.ts +++ b/packages/core/test/integration/query-interface/add-show-remove-constraint.test.ts @@ -78,6 +78,8 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { 'Expected error to be an instance of AggregateError', ); err = error.errors.at(-1); + } else if (dialect === 'oracle') { + expect(error).to.be.instanceOf(UnknownConstraintError); } else { assert( err instanceof UnknownConstraintError, @@ -112,14 +114,13 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { constraintName: 'custom_constraint_name', constraintType: 'UNIQUE', ...(['mssql', 'postgres'].includes(dialect) && { tableCatalog: 'sequelize_test' }), - tableSchema: defaultSchema, + ...(dialect !== 'oracle' && { tableSchema: defaultSchema }), tableName: 'actors', - columnNames: ['name', 'age'], + columnNames: dialect === 'oracle' ? ['age', 'name'] : ['name', 'age'], ...(sequelize.dialect.supports.constraints.deferrable && { deferrable: 'INITIALLY_IMMEDIATE', }), }); - await queryInterface.removeConstraint('actors', 'custom_constraint_name'); const constraintsAfterRemove = await queryInterface.showConstraints('actors', { constraintName: 'custom_constraint_name', @@ -156,14 +157,17 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { constraintName: 'custom_constraint_name', constraintType: 'FOREIGN KEY', ...(['mssql', 'postgres'].includes(dialect) && { tableCatalog: 'sequelize_test' }), - tableSchema: defaultSchema, + ...(dialect !== 'oracle' && { tableSchema: defaultSchema }), tableName: 'actors', columnNames: ['level_id'], referencedTableName: 'levels', referencedTableSchema: defaultSchema, referencedColumnNames: ['id'], deleteAction: 'CASCADE', - updateAction: dialect === 'mariadb' ? 'RESTRICT' : dialect === 'sqlite3' ? '' : 'NO ACTION', + ...(dialect !== 'oracle' && { + updateAction: + dialect === 'mariadb' ? 'RESTRICT' : dialect === 'sqlite3' ? '' : 'NO ACTION', + }), ...(sequelize.dialect.supports.constraints.deferrable && { deferrable: 'INITIALLY_IMMEDIATE', }), @@ -186,7 +190,7 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { constraintName: ['mariadb', 'mysql'].includes(dialect) ? 'PRIMARY' : 'pk_levels', constraintType: 'PRIMARY KEY', ...(['mssql', 'postgres'].includes(dialect) && { tableCatalog: 'sequelize_test' }), - tableSchema: defaultSchema, + ...(dialect !== 'oracle' && { tableSchema: defaultSchema }), tableName: 'levels', columnNames: ['id'], ...(sequelize.dialect.supports.constraints.deferrable && { @@ -232,14 +236,17 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { constraintName: 'custom_constraint_name', constraintType: 'FOREIGN KEY', ...(['mssql', 'postgres'].includes(dialect) && { tableCatalog: 'sequelize_test' }), - tableSchema: defaultSchema, + ...(dialect !== 'oracle' && { tableSchema: defaultSchema }), tableName: 'actors', columnNames: ['level_id', 'manager_id'], referencedTableSchema: defaultSchema, referencedTableName: 'levels', referencedColumnNames: ['id', 'manager_id'], deleteAction: 'CASCADE', - updateAction: dialect === 'mariadb' ? 'RESTRICT' : dialect === 'sqlite3' ? '' : 'NO ACTION', + ...(dialect !== 'oracle' && { + updateAction: + dialect === 'mariadb' ? 'RESTRICT' : dialect === 'sqlite3' ? '' : 'NO ACTION', + }), ...(sequelize.dialect.supports.constraints.deferrable && { deferrable: 'INITIALLY_IMMEDIATE', }), @@ -261,9 +268,9 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { constraintName: ['mariadb', 'mysql'].includes(dialect) ? 'PRIMARY' : 'pk_levels', constraintType: 'PRIMARY KEY', ...(['mssql', 'postgres'].includes(dialect) && { tableCatalog: 'sequelize_test' }), - tableSchema: defaultSchema, + ...(dialect !== 'oracle' && { tableSchema: defaultSchema }), tableName: 'levels', - columnNames: ['id', 'manager_id'], + columnNames: dialect === 'oracle' ? ['manager_id', 'id'] : ['id', 'manager_id'], ...(sequelize.dialect.supports.constraints.deferrable && { deferrable: 'INITIALLY_IMMEDIATE', }), @@ -296,7 +303,7 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { field: 'id', }, onDelete: 'CASCADE', - onUpdate: 'CASCADE', + onUpdate: dialect !== 'oracle' ? 'CASCADE' : undefined, }); const constraintType = await queryInterface.showConstraints('actors', { @@ -344,7 +351,7 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { const constraintType = await queryInterface.showConstraints('actors', { constraintType: 'CHECK', }); - if (dialect === 'postgres') { + if (dialect === 'postgres' || dialect === 'oracle') { // Postgres adds a CHECK constraint for each column with not null expect(constraintType).to.have.length(6); expect(constraintType[5].constraintType).to.equal('CHECK'); @@ -360,21 +367,24 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { expect(constraints[0]).to.deep.equal({ ...(['mssql', 'postgres'].includes(dialect) && { constraintCatalog: 'sequelize_test' }), constraintSchema: defaultSchema, + ...(['oracle'].includes(dialect) && { columnNames: ['age'] }), constraintName: 'custom_constraint_name', constraintType: 'CHECK', ...(['mssql', 'postgres'].includes(dialect) && { tableCatalog: 'sequelize_test' }), - tableSchema: defaultSchema, + ...(dialect !== 'oracle' && { tableSchema: defaultSchema }), tableName: 'actors', - definition: - dialect === 'mssql' - ? '([age]>(10))' - : dialect === 'db2' - ? '"age" > 10' - : dialect === 'postgres' - ? '(age > 10)' - : ['mysql', 'sqlite3'].includes(dialect) - ? '(`age` > 10)' - : '`age` > 10', + ...(dialect !== 'oracle' && { + definition: + dialect === 'mssql' + ? '([age]>(10))' + : dialect === 'db2' + ? '"age" > 10' + : dialect === 'postgres' + ? '(age > 10)' + : ['mysql', 'sqlite3'].includes(dialect) + ? '(`age` > 10)' + : '`age` > 10', + }), ...(sequelize.dialect.supports.constraints.deferrable && { deferrable: 'INITIALLY_IMMEDIATE', }), @@ -526,15 +536,17 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { constraintName: 'custom_constraint_name', constraintType: 'FOREIGN KEY', ...(['mssql', 'postgres'].includes(dialect) && { tableCatalog: 'sequelize_test' }), - tableSchema: schema, + ...(dialect !== 'oracle' && { tableSchema: schema }), tableName: 'actors', columnNames: ['level_id'], referencedTableSchema: schema, referencedTableName: 'levels', referencedColumnNames: ['id'], deleteAction: 'CASCADE', - updateAction: - dialect === 'mariadb' ? 'RESTRICT' : dialect === 'sqlite3' ? '' : 'NO ACTION', + ...(dialect !== 'oracle' && { + updateAction: + dialect === 'mariadb' ? 'RESTRICT' : dialect === 'sqlite3' ? '' : 'NO ACTION', + }), ...(sequelize.dialect.supports.constraints.deferrable && { deferrable: 'INITIALLY_IMMEDIATE', }), @@ -561,7 +573,7 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { constraintName: ['mariadb', 'mysql'].includes(dialect) ? 'PRIMARY' : 'pk_levels', constraintType: 'PRIMARY KEY', ...(['mssql', 'postgres'].includes(dialect) && { tableCatalog: 'sequelize_test' }), - tableSchema: schema, + ...(dialect !== 'oracle' && { tableSchema: schema }), tableName: 'levels', columnNames: ['id'], ...(sequelize.dialect.supports.constraints.deferrable && { @@ -647,15 +659,17 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { constraintName: 'custom_constraint_name', constraintType: 'FOREIGN KEY', ...(['mssql', 'postgres'].includes(dialect) && { tableCatalog: 'sequelize_test' }), - tableSchema: schema, + ...(dialect !== 'oracle' && { tableSchema: schema }), tableName: 'actors', columnNames: ['level_id'], referencedTableSchema: schema, referencedTableName: 'levels', referencedColumnNames: ['id'], deleteAction: 'CASCADE', - updateAction: - dialect === 'mariadb' ? 'RESTRICT' : dialect === 'sqlite3' ? '' : 'NO ACTION', + ...(dialect !== 'oracle' && { + updateAction: + dialect === 'mariadb' ? 'RESTRICT' : dialect === 'sqlite3' ? '' : 'NO ACTION', + }), ...(sequelize.dialect.supports.constraints.deferrable && { deferrable: 'INITIALLY_IMMEDIATE', }), @@ -675,15 +689,17 @@ describe('QueryInterface#{add,show,removeConstraint}', () => { constraintName: 'custom_constraint_name', constraintType: 'FOREIGN KEY', ...(['mssql', 'postgres'].includes(dialect) && { tableCatalog: 'sequelize_test' }), - tableSchema: sequelize.dialect.getDefaultSchema(), + ...(dialect !== 'oracle' && { tableSchema: sequelize.dialect.getDefaultSchema() }), tableName: 'actors', columnNames: ['level_id'], referencedTableSchema: sequelize.dialect.getDefaultSchema(), referencedTableName: 'levels', referencedColumnNames: ['id'], deleteAction: 'CASCADE', - updateAction: - dialect === 'mariadb' ? 'RESTRICT' : dialect === 'sqlite3' ? '' : 'NO ACTION', + ...(dialect !== 'oracle' && { + updateAction: + dialect === 'mariadb' ? 'RESTRICT' : dialect === 'sqlite3' ? '' : 'NO ACTION', + }), ...(sequelize.dialect.supports.constraints.deferrable && { deferrable: 'INITIALLY_IMMEDIATE', }), diff --git a/packages/core/test/integration/query-interface/changeColumn.test.js b/packages/core/test/integration/query-interface/changeColumn.test.js index eab6d6279758..ccdf90198296 100644 --- a/packages/core/test/integration/query-interface/changeColumn.test.js +++ b/packages/core/test/integration/query-interface/changeColumn.test.js @@ -51,6 +51,8 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { if (['postgres', 'postgres-native', 'mssql', 'db2'].includes(dialect)) { expect(table.currency.type).to.equal('REAL'); + } else if (dialect === 'oracle') { + expect(table.currency.type).to.equal('BINARY_FLOAT'); } else { expect(table.currency.type).to.equal('FLOAT'); } @@ -89,6 +91,8 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { if (['postgres', 'postgres-native', 'mssql', 'sqlite3', 'db2'].includes(dialect)) { expect(table.currency.type).to.equal('REAL'); + } else if (dialect === 'oracle') { + expect(table.currency.type).to.equal('BINARY_FLOAT'); } else { expect(table.currency.type).to.equal('FLOAT'); } @@ -251,7 +255,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { expect(describedTable.level_id.allowNull).to.equal(true); }); - if (!['db2', 'ibmi', 'sqlite3'].includes(dialect)) { + if (!['db2', 'ibmi', 'sqlite3', 'oracle'].includes(dialect)) { it('should change the comment of column', async function () { const describedTable = await this.queryInterface.describeTable({ tableName: 'users', diff --git a/packages/core/test/integration/query-interface/describeTable.test.js b/packages/core/test/integration/query-interface/describeTable.test.js index ac8e4036f5ec..0722056ce413 100644 --- a/packages/core/test/integration/query-interface/describeTable.test.js +++ b/packages/core/test/integration/query-interface/describeTable.test.js @@ -117,6 +117,9 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { case 'db2': assertVal = 'VARCHAR'; break; + case 'oracle': + assertVal = 'NVARCHAR2'; + break; } expect(username.type).to.equal(assertVal); @@ -124,6 +127,7 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { switch (dialect) { case 'sqlite3': + case 'oracle': expect(username.defaultValue).to.be.undefined; break; default: @@ -151,12 +155,16 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { case 'ibmi': assertVal = 'SMALLINT'; break; + case 'oracle': + assertVal = 'CHAR'; + break; } expect(isAdmin.type).to.equal(assertVal); expect(isAdmin.allowNull).to.be.true; switch (dialect) { case 'sqlite3': + case 'oracle': expect(isAdmin.defaultValue).to.be.undefined; break; default: @@ -168,6 +176,8 @@ describe(Support.getTestDialectTeaser('QueryInterface'), () => { expect(enumVals.special).to.have.length(2); } else if (dialect === 'mysql') { expect(enumVals.type).to.eql("ENUM('hello','world')"); + } else if (dialect === 'oracle') { + expect(enumVals.type).to.eql('VARCHAR2'); } if (['postgres', 'mysql', 'mssql'].includes(dialect)) { diff --git a/packages/core/test/integration/query-interface/list-tables.test.ts b/packages/core/test/integration/query-interface/list-tables.test.ts index aa1ecf9e8f43..162b5557f922 100644 --- a/packages/core/test/integration/query-interface/list-tables.test.ts +++ b/packages/core/test/integration/query-interface/list-tables.test.ts @@ -33,14 +33,36 @@ describe('QueryInterface#listTables', () => { throw error; } } + } else if (dialectName === 'oracle') { + const plsql = [ + 'BEGIN', + 'EXECUTE IMMEDIATE', + "'DROP VIEW V_Fail';", + 'EXCEPTION WHEN OTHERS THEN', + ' IF SQLCODE != -942 THEN', + ' RAISE;', + ' END IF;', + 'END;', + ].join(' '); + await sequelize.query(plsql); } else { await sequelize.queryRaw('DROP VIEW IF EXISTS V_Fail;'); } } + const fromQuery = () => { + if (['db2', 'ibmi'].includes(dialectName)) { + return 'FROM SYSIBM.SYSDUMMY1'; + } else if (dialectName === 'oracle') { + return 'FROM DUAL'; + } + + return ''; + }; + await queryInterface.createTable('my_test_table', { name: DataTypes.STRING }); await cleanup(); - const sql = `CREATE VIEW V_Fail AS SELECT 1 Id${['db2', 'ibmi'].includes(dialectName) ? ' FROM SYSIBM.SYSDUMMY1' : ''};`; + const sql = `CREATE VIEW V_Fail AS SELECT 1 Id ${fromQuery()};`; await sequelize.queryRaw(sql); const allTables = await queryInterface.listTables(); const tableNames = allTables.map(v => v.tableName); diff --git a/packages/core/test/integration/query-interface/remove-column.test.ts b/packages/core/test/integration/query-interface/remove-column.test.ts index e578ad502c80..fe74622c583a 100644 --- a/packages/core/test/integration/query-interface/remove-column.test.ts +++ b/packages/core/test/integration/query-interface/remove-column.test.ts @@ -214,15 +214,17 @@ describe(getTestDialectTeaser('QueryInterface#removeColumn'), () => { constraintName: dialectName === 'sqlite3' ? 'FOREIGN' : 'actors_level_id_fkey', constraintType: 'FOREIGN KEY', ...(['mssql', 'postgres'].includes(dialectName) && { tableCatalog: 'sequelize_test' }), - tableSchema: defaultSchema, + ...(dialectName !== 'oracle' && { tableSchema: defaultSchema }), tableName: 'actors', columnNames: ['level_id'], referencedTableName: 'level', referencedTableSchema: defaultSchema, referencedColumnNames: ['id'], deleteAction: 'CASCADE', - updateAction: - dialectName === 'mariadb' ? 'RESTRICT' : dialectName === 'sqlite3' ? '' : 'NO ACTION', + ...(dialectName !== 'oracle' && { + updateAction: + dialectName === 'mariadb' ? 'RESTRICT' : dialectName === 'sqlite3' ? '' : 'NO ACTION', + }), ...(sequelize.dialect.supports.constraints.deferrable && { deferrable: 'INITIALLY_IMMEDIATE', }), diff --git a/packages/core/test/integration/query-interface/schemas.test.ts b/packages/core/test/integration/query-interface/schemas.test.ts index a172ed1fd931..5567d27dc154 100644 --- a/packages/core/test/integration/query-interface/schemas.test.ts +++ b/packages/core/test/integration/query-interface/schemas.test.ts @@ -179,13 +179,16 @@ describe('QueryInterface#{create,drop,list}Schema', () => { expect(postDeletionSchemas).to.not.include(testSchema, 'dropSchema did not drop testSchema'); }); + // For Oracle Database, users are considered as schema. listSchemas() doesn't allow to fetch the + // defaultSchema. it('shows all schemas', async () => { await queryInterface.createSchema(testSchema); const allSchemas = await queryInterface.listSchemas(); - const expected = !dialect.supports.multiDatabases - ? [sequelize.dialect.getDefaultSchema(), testSchema] - : [testSchema]; + const expected = + !dialect.supports.multiDatabases && dialect.name !== 'oracle' + ? [sequelize.dialect.getDefaultSchema(), testSchema] + : [testSchema]; expect(allSchemas.sort()).to.deep.eq(expected.sort(basicComparator())); }); diff --git a/packages/core/test/integration/sequelize.test.js b/packages/core/test/integration/sequelize.test.js index 3cf3982e7c1f..699dabfa2968 100644 --- a/packages/core/test/integration/sequelize.test.js +++ b/packages/core/test/integration/sequelize.test.js @@ -21,7 +21,7 @@ const { CONFIG } = require('../config/config'); const dialect = getTestDialect(); const qq = str => { - if (['postgres', 'mssql', 'db2', 'ibmi'].includes(dialect)) { + if (['postgres', 'mssql', 'db2', 'ibmi', 'oracle'].includes(dialect)) { return `"${str}"`; } @@ -61,6 +61,9 @@ const badUsernameConfig = { snowflake: { account: 'bad_account', }, + oracle: { + username: 'bad_user', + }, }; const noPasswordConfig = { @@ -92,6 +95,9 @@ const noPasswordConfig = { snowflake: { password: null, }, + oracle: { + password: null, + }, }; const badAddressConfig = { @@ -117,6 +123,9 @@ const badAddressConfig = { ibmi: { system: 'bad-address', }, + oracle: { + port: 9999, + }, }; describe(getTestDialectTeaser('Sequelize'), () => { @@ -384,6 +393,12 @@ describe(getTestDialectTeaser('Sequelize'), () => { break; } + case 'oracle': { + expect(error.message).to.include('NJS-007'); + + break; + } + case 'ibmi': { expect(error.message).to.equal('[odbc] Error connecting to the database'); expect(error.cause.odbcErrors[0].message).to.include( diff --git a/packages/core/test/integration/sequelize/query.test.js b/packages/core/test/integration/sequelize/query.test.js index 00357119657a..a5db0bcd23d3 100644 --- a/packages/core/test/integration/sequelize/query.test.js +++ b/packages/core/test/integration/sequelize/query.test.js @@ -26,7 +26,7 @@ const dialectName = getTestDialect(); const queryGenerator = sequelize.queryGenerator; const qq = str => { - if (['postgres', 'mssql', 'db2', 'ibmi'].includes(dialectName)) { + if (['postgres', 'mssql', 'db2', 'ibmi', 'oracle'].includes(dialectName)) { return `"${str}"`; } @@ -37,6 +37,25 @@ const qq = str => { return str; }; +const fromQuery = () => { + let query = ''; + if (dialectName === 'oracle') { + query += ' FROM DUAL'; + } else if (dialectName === 'ibmi') { + query += ' FROM SYSIBM.SYSDUMMY1'; + } + + return query; +}; + +const dateLiteral = str => { + if (dialectName === 'oracle') { + return `to_date('${str}','YYYY-MM-DD HH24:MI:SS')`; + } + + return `'${str}'`; +}; + describe(getTestDialectTeaser('Sequelize'), () => { allowDeprecationsInSuite(['SEQUELIZE0023']); @@ -57,11 +76,11 @@ describe(getTestDialectTeaser('Sequelize'), () => { }, }); - this.insertQuery = `INSERT INTO ${qq(this.User.tableName)} (username, email_address, ${qq( + 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 (['db2', 'ibmi'].includes(dialectName)) { 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')`; @@ -100,8 +119,10 @@ describe(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: + dialectName === 'oracle' ? new Date('2010-10-10 00:00:00') : '2010-10-10 00:00:00', + updatedAt: + dialectName === 'oracle' ? new Date('2010-10-10 00:00:00') : '2010-10-10 00:00:00', }; const spy = sinon.spy(); @@ -135,7 +156,7 @@ describe(getTestDialectTeaser('Sequelize'), () => { benchmark: true, }); - await sequelize.query(`select 1${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''};`); + await sequelize.query(`select 1${fromQuery()};`); 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; @@ -144,13 +165,10 @@ describe(getTestDialectTeaser('Sequelize'), () => { it('executes a query with benchmarking option and custom logger', async function () { const logger = sinon.spy(); - await this.sequelize.query( - `select 1${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''};`, - { - logging: logger, - benchmark: true, - }, - ); + await this.sequelize.query(`select 1${fromQuery()};`, { + logging: logger, + benchmark: true, + }); expect(logger.calledOnce).to.be.true; expect(logger.args[0][0]).to.be.match(/Executed \(\d*|default\): select 1;/); @@ -163,12 +181,9 @@ describe(getTestDialectTeaser('Sequelize'), () => { logging: logger, }); - await sequelize.query( - `select 1${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''};`, - { - queryLabel: 'tricky select', - }, - ); + await sequelize.query(`select 1${fromQuery()};`, { + queryLabel: 'tricky select', + }); expect(logger.calledOnce).to.be.true; expect(logger.args[0][0]).to.be.match( /^tricky select[\n]Executing \((\d*|default)\): select 1/, @@ -181,12 +196,9 @@ describe(getTestDialectTeaser('Sequelize'), () => { logging: logger, }); - await sequelize.query( - `select 1${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''};`, - { - queryLabel: '', - }, - ); + await sequelize.query(`select 1${fromQuery()};`, { + queryLabel: '', + }); expect(logger.calledOnce).to.be.true; expect(logger.args[0][0]).to.be.match(/^Executing \((\d*|default)\): select 1/); }); @@ -198,12 +210,9 @@ describe(getTestDialectTeaser('Sequelize'), () => { benchmark: true, }); - await sequelize.query( - `select 1${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''};`, - { - queryLabel: 'tricky select', - }, - ); + await sequelize.query(`select 1${fromQuery()};`, { + queryLabel: 'tricky select', + }); expect(logger.calledOnce).to.be.true; expect(logger.args[0][0]).to.be.match( /^tricky select[\n]Executed \((\d*|default)\): select 1/, @@ -306,7 +315,7 @@ describe(getTestDialectTeaser('Sequelize'), () => { } await vars.sequelize.query( - `select $1${typeCast} as foo, $2${typeCast} as bar${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select $1${typeCast} as foo, $2${typeCast} as bar${fromQuery()}`, { bind: ['foo', 'bar'], logging: s => { @@ -518,7 +527,7 @@ describe(getTestDialectTeaser('Sequelize'), () => { it('emits full stacktraces for unique constraint error', async function () { let query; - if (['db2', 'ibmi'].includes(dialectName)) { + if (['db2', 'ibmi', 'oracle'].includes(dialectName)) { query = `INSERT INTO ${qq(this.User.tableName)} ("username", "email_address", ${qq( 'createdAt', )}, ${qq( @@ -542,15 +551,19 @@ describe(getTestDialectTeaser('Sequelize'), () => { error = error_; } - expect(error).to.be.instanceOf(UniqueConstraintError); - expect(error.stack).to.contain('query.test'); + if (dialectName === 'oracle') { + expect(error).to.be.instanceOf(DatabaseError); + } else { + expect(error).to.be.instanceOf(UniqueConstraintError); + expect(error.stack).to.contain('query.test'); + } }); it('emits full stacktraces for constraint validation error', async function () { let error = null; try { let query; - if (['db2', 'ibmi'].includes(dialectName)) { + if (['db2', 'ibmi', 'oracle'].includes(dialectName)) { query = `INSERT INTO ${qq(this.UserVisit.tableName)} ("user_id", "visited_at", ${qq( 'createdAt', )}, ${qq( @@ -569,8 +582,12 @@ describe(getTestDialectTeaser('Sequelize'), () => { error = error_; } - expect(error).to.be.instanceOf(ForeignKeyConstraintError); - expect(error.stack).to.contain('query.test'); + if (dialectName === 'oracle') { + expect(error).to.be.instanceOf(DatabaseError); + } else { + expect(error).to.be.instanceOf(ForeignKeyConstraintError); + expect(error.stack).to.contain('query.test'); + } }); }); } @@ -747,14 +764,14 @@ describe(getTestDialectTeaser('Sequelize'), () => { // dialects in which the following values will be returned as bigints instead of ints const isBigInt = dialectName === 'mysql'; it('dot separated attributes when doing a raw query without nest', async function () { - const sql = `select 1 as ${queryGenerator.quoteIdentifier('foo.bar.baz')}${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`; + const sql = `select 1 as ${queryGenerator.quoteIdentifier('foo.bar.baz')}${fromQuery()}`; const results = await this.sequelize.query(sql, { raw: true, nest: false }); expect(results[0]).to.deep.equal([{ 'foo.bar.baz': isBigInt ? '1' : 1 }]); }); it('destructs dot separated attributes when doing a raw query using nest', async function () { - const sql = `select 1 as ${queryGenerator.quoteIdentifier('foo.bar.baz')}${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`; + const sql = `select 1 as ${queryGenerator.quoteIdentifier('foo.bar.baz')}${fromQuery()}`; const result = await this.sequelize.query(sql, { raw: true, nest: true }); expect(result).to.deep.equal([{ foo: { bar: { baz: isBigInt ? '1' : 1 } } }]); @@ -763,7 +780,7 @@ describe(getTestDialectTeaser('Sequelize'), () => { it('replaces token with the passed array', async function () { const expected = [{ foo: isBigInt ? '1' : 1, bar: isBigInt ? '2' : 2 }]; const result = await this.sequelize.query( - `select ? as ${queryGenerator.quoteIdentifier('foo')}, ? as ${queryGenerator.quoteIdentifier('bar')}${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select ? as ${queryGenerator.quoteIdentifier('foo')}, ? as ${queryGenerator.quoteIdentifier('bar')}${fromQuery()}`, { type: this.sequelize.QueryTypes.SELECT, replacements: [1, 2] }, ); expect(result).to.deep.equal(expected); @@ -774,7 +791,7 @@ describe(getTestDialectTeaser('Sequelize'), () => { await expect( this.sequelize .query( - `select :one as ${queryGenerator.quoteIdentifier('foo')}, :two as ${queryGenerator.quoteIdentifier('bar')}${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select :one as ${queryGenerator.quoteIdentifier('foo')}, :two as ${queryGenerator.quoteIdentifier('bar')}${fromQuery()}`, { raw: true, replacements: { one: 1, two: 2 } }, ) .then(obj => obj[0]), @@ -786,7 +803,7 @@ describe(getTestDialectTeaser('Sequelize'), () => { await expect( this.sequelize .query( - `select :one as ${queryGenerator.quoteIdentifier('foo')}, :two as ${queryGenerator.quoteIdentifier('bar')}, '00:00' as ${queryGenerator.quoteIdentifier('baz')}${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select :one as ${queryGenerator.quoteIdentifier('foo')}, :two as ${queryGenerator.quoteIdentifier('bar')}, '00:00' as ${queryGenerator.quoteIdentifier('baz')}${fromQuery()}`, { raw: true, replacements: { one: 1, two: 2 } }, ) .then(obj => obj[0]), @@ -800,7 +817,7 @@ describe(getTestDialectTeaser('Sequelize'), () => { await expect( this.sequelize .query( - `select :one as ${queryGenerator.quoteIdentifier('foo')}, :two as ${queryGenerator.quoteIdentifier('bar')}, :one as ${queryGenerator.quoteIdentifier('baz')}${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select :one as ${queryGenerator.quoteIdentifier('foo')}, :two as ${queryGenerator.quoteIdentifier('bar')}, :one as ${queryGenerator.quoteIdentifier('baz')}${fromQuery()}`, { raw: true, replacements: { one: 1, two: 2 } }, ) .then(obj => obj[0]), @@ -812,7 +829,7 @@ describe(getTestDialectTeaser('Sequelize'), () => { await expect( this.sequelize .query( - `select :one as ${queryGenerator.quoteIdentifier('foo')}, :two as ${queryGenerator.quoteIdentifier('bar')}${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select :one as ${queryGenerator.quoteIdentifier('foo')}, :two as ${queryGenerator.quoteIdentifier('bar')}${fromQuery()}`, { raw: true, replacements: { one: 1, two: null } }, ) .then(obj => obj[0]), @@ -828,7 +845,7 @@ describe(getTestDialectTeaser('Sequelize'), () => { const typeCast = ['postgres', 'db2'].includes(dialectName) ? '::int' : ''; let logSql; const result = await this.sequelize.query( - `select $1${typeCast} as ${queryGenerator.quoteIdentifier('foo')}, $2${typeCast} as ${queryGenerator.quoteIdentifier('bar')}${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select $1${typeCast} as ${queryGenerator.quoteIdentifier('foo')}, $2${typeCast} as ${queryGenerator.quoteIdentifier('bar')}${fromQuery()}`, { type: this.sequelize.QueryTypes.SELECT, bind: [1, 2], @@ -849,7 +866,7 @@ describe(getTestDialectTeaser('Sequelize'), () => { const typeCast = ['postgres', 'db2'].includes(dialectName) ? '::int' : ''; let logSql; const result = await this.sequelize.query( - `select $one${typeCast} as ${queryGenerator.quoteIdentifier('foo')}, $two${typeCast} as ${queryGenerator.quoteIdentifier('bar')}${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select $one${typeCast} as ${queryGenerator.quoteIdentifier('foo')}, $two${typeCast} as ${queryGenerator.quoteIdentifier('bar')}${fromQuery()}`, { raw: true, bind: { one: 1, two: 2 }, @@ -868,12 +885,12 @@ describe(getTestDialectTeaser('Sequelize'), () => { } }); - if (dialectName !== 'db2') { + if (!['db2', 'oracle'].includes(dialectName)) { it('binds named parameters with the passed object using the same key twice', async function () { const typeCast = dialectName === 'postgres' ? '::int' : ''; let logSql; const result = await this.sequelize.query( - `select $one${typeCast} as foo, $two${typeCast} as bar, $one${typeCast} as baz${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select $one${typeCast} as foo, $two${typeCast} as bar, $one${typeCast} as baz${fromQuery()}`, { raw: true, bind: { one: 1, two: 2 }, @@ -899,10 +916,10 @@ describe(getTestDialectTeaser('Sequelize'), () => { it('binds named parameters with the passed object having a null property', async function () { const typeCast = ['postgres', 'db2'].includes(dialectName) ? '::int' : ''; const result = await this.sequelize.query( - `select $one${typeCast} as foo, $two${typeCast} as bar${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select $one${typeCast} as foo, $two${typeCast} as bar${fromQuery()}`, { raw: true, bind: { one: 1, two: null } }, ); - const expected = ['db2', 'ibmi'].includes(dialectName) + const expected = ['db2', 'ibmi', 'oracle'].includes(dialectName) ? [{ FOO: 1, BAR: null }] : [{ foo: 1, bar: null }]; expect(result[0]).to.deep.equal(expected); @@ -913,7 +930,7 @@ describe(getTestDialectTeaser('Sequelize'), () => { const typeCast = ['postgres', 'db2'].includes(dialectName) ? '::int' : ''; let logSql; const result = await this.sequelize.query( - `select $1${typeCast} as foo, '$$ / $$1' as bar${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select $1${typeCast} as foo, '$$ / $$1' as bar${fromQuery()}`, { raw: true, bind: [1], @@ -922,7 +939,7 @@ describe(getTestDialectTeaser('Sequelize'), () => { }, }, ); - const expected = ['db2', 'ibmi'].includes(dialectName) + const expected = ['db2', 'ibmi', 'oracle'].includes(dialectName) ? [{ FOO: 1, BAR: '$$ / $$1' }] : [{ foo: 1, bar: '$$ / $$1' }]; expect(result[0]).to.deep.equal(expected); @@ -935,10 +952,10 @@ describe(getTestDialectTeaser('Sequelize'), () => { it('does not transform $$ in strings (named)', async function () { const typeCast = ['postgres', 'db2'].includes(dialectName) ? '::int' : ''; const result = await this.sequelize.query( - `select $one${typeCast} as foo, '$$ / $$one' as bar${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select $one${typeCast} as foo, '$$ / $$one' as bar${fromQuery()}`, { raw: true, bind: { one: 1 } }, ); - const expected = ['db2', 'ibmi'].includes(dialectName) + const expected = ['db2', 'ibmi', 'oracle'].includes(dialectName) ? [{ FOO: 1, BAR: '$$ / $$one' }] : [{ foo: 1, bar: '$$ / $$one' }]; expect(result[0]).to.deep.equal(expected); @@ -947,23 +964,24 @@ describe(getTestDialectTeaser('Sequelize'), () => { it(`does not treat a $ as a bind param if it's in the middle of an identifier`, async function () { const typeCast = ['postgres', 'db2'].includes(dialectName) ? '::int' : ''; const result = await this.sequelize.query( - `select $one${typeCast} as foo$bar${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, + `select $one${typeCast} as foo$bar${fromQuery()}`, { raw: true, bind: { one: 1 } }, ); - const expected = ['db2', 'ibmi'].includes(dialectName) + const expected = ['db2', 'ibmi', 'oracle'].includes(dialectName) ? [{ FOO$BAR: 1 }] : [{ foo$bar: 1 }]; expect(result[0]).to.deep.equal(expected); }); } - if (['postgres', 'sqlite3', 'mssql'].includes(dialectName)) { + if (['postgres', 'sqlite3', 'mssql', 'oracle'].includes(dialectName)) { it('does not improperly escape arrays of strings bound to named parameters', async function () { - const result = await this.sequelize.query('select :stringArray as foo', { + const result = await this.sequelize.query(`select :stringArray as foo${fromQuery()}`, { raw: true, replacements: { stringArray: sql.list(['"string"']) }, }); - expect(result[0]).to.deep.equal([{ foo: '"string"' }]); + const expectedData = dialectName !== 'oracle' ? { foo: '"string"' } : { FOO: '"string"' }; + expect(result[0]).to.deep.equal([expectedData]); }); } @@ -971,11 +989,11 @@ describe(getTestDialectTeaser('Sequelize'), () => { let datetime = dialectName === 'sqlite3' ? "date('now')" : 'NOW()'; if (dialectName === 'mssql') { datetime = 'GETDATE()'; + } else if (dialectName === 'oracle') { + datetime = 'SYSDATE'; } - const [result] = await this.sequelize.query( - `SELECT ${datetime} AS t${dialectName === 'ibmi' ? ' FROM SYSIBM.SYSDUMMY1' : ''}`, - ); + const [result] = await this.sequelize.query(`SELECT ${datetime} AS t${fromQuery()}`); expect(dayjs(result[0].t).isValid()).to.be.true; }); diff --git a/packages/core/test/integration/sequelize/transaction.test.ts b/packages/core/test/integration/sequelize/transaction.test.ts index e1e52400851c..74e10c66c60a 100644 --- a/packages/core/test/integration/sequelize/transaction.test.ts +++ b/packages/core/test/integration/sequelize/transaction.test.ts @@ -340,7 +340,7 @@ describe(getTestDialectTeaser('Sequelize#transaction'), () => { } // These dialects do not allow dirty reads with isolation level "READ UNCOMMITTED". - if (!['postgres', 'sqlite3'].includes(dialectName)) { + if (!['postgres', 'sqlite3', 'oracle'].includes(dialectName)) { it('should allow dirty read with isolation level "READ UNCOMMITTED"', async () => { const { User, transactionSequelize } = vars; const t1 = await transactionSequelize.startUnmanagedTransaction({ @@ -427,7 +427,7 @@ describe(getTestDialectTeaser('Sequelize#transaction'), () => { } // These dialects do not allow phantom reads with isolation level "REPEATABLE READ" as they use snapshot rather than locking. - if (['mariadb', 'mysql', 'postgres'].includes(dialectName)) { + if (['mariadb', 'mysql', 'postgres', 'oracle'].includes(dialectName)) { it('should not read newly committed rows when using the REPEATABLE READ isolation level', async () => { const { User, transactionSequelize } = vars; @@ -479,7 +479,7 @@ describe(getTestDialectTeaser('Sequelize#transaction'), () => { } // PostgreSQL is excluded because it detects Serialization Failure on commit instead of acquiring locks on the read rows - if (!['postgres'].includes(dialectName)) { + if (!['postgres', 'oracle'].includes(dialectName)) { it('should block updates after reading a row using SERIALIZABLE', async () => { const { User, transactionSequelize } = vars; const transactionSpy = sinon.spy(); @@ -591,6 +591,9 @@ describe(getTestDialectTeaser('Sequelize#transaction'), () => { case 'mssql': query = "WAITFOR DELAY '00:00:02';"; break; + case 'oracle': + query = 'BEGIN DBMS_SESSION.sleep(2); END;'; + break; default: query = 'select sleep(2);'; break; diff --git a/packages/core/test/integration/transaction.test.js b/packages/core/test/integration/transaction.test.js index 625ab8ca541b..8c027e4ba3ae 100644 --- a/packages/core/test/integration/transaction.test.js +++ b/packages/core/test/integration/transaction.test.js @@ -20,6 +20,14 @@ const current = Support.sequelize; const delay = require('delay'); const pSettle = require('p-settle'); +const fromQuery = () => { + if (dialect === 'oracle') { + return ' FROM DUAL'; + } + + return ''; +}; + describe(Support.getTestDialectTeaser('Transaction'), () => { if (!current.dialect.supports.transactions) { return; @@ -108,7 +116,10 @@ describe(Support.getTestDialectTeaser('Transaction'), () => { transaction.afterRollback(afterRollback); transaction.afterTransaction(afterTransaction); - return this.sequelize.query('SELECT 1+1', { transaction, type: QueryTypes.SELECT }); + return this.sequelize.query(`SELECT 1+1${fromQuery()}`, { + transaction, + type: QueryTypes.SELECT, + }); }); expect(afterCommit).to.have.been.calledOnce; @@ -260,31 +271,31 @@ describe(Support.getTestDialectTeaser('Transaction'), () => { it('does not allow queries after commit', async function () { const t = await this.sequelize.startUnmanagedTransaction(); - await this.sequelize.query('SELECT 1+1', { transaction: t, raw: true }); + await this.sequelize.query(`SELECT 1+1${fromQuery()}`, { transaction: t, raw: true }); await t.commit(); - await expect(this.sequelize.query('SELECT 1+1', { transaction: t, raw: true })) + await expect(this.sequelize.query(`SELECT 1+1${fromQuery()}`, { 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'); + .that.equal(`SELECT 1+1${fromQuery()}`); }); it('does not allow queries immediately after commit call', async function () { await expect( (async () => { const t = await this.sequelize.startUnmanagedTransaction(); - await this.sequelize.query('SELECT 1+1', { transaction: t, raw: true }); + await this.sequelize.query(`SELECT 1+1${fromQuery()}`, { 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 })) + expect(this.sequelize.query(`SELECT 1+1${fromQuery()}`, { 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'), + .that.equal(`SELECT 1+1${fromQuery()}`), ]); })(), ).to.be.eventually.fulfilled; @@ -294,10 +305,13 @@ describe(Support.getTestDialectTeaser('Transaction'), () => { await expect( (async () => { const t = await this.sequelize.startUnmanagedTransaction(); - await this.sequelize.query('SELECT 1+1', { transaction: t, raw: true }); + await this.sequelize.query(`SELECT 1+1${fromQuery()}`, { transaction: t, raw: true }); await t.rollback(); - return await this.sequelize.query('SELECT 1+1', { transaction: t, raw: true }); + return await this.sequelize.query(`SELECT 1+1${fromQuery()}`, { + transaction: t, + raw: true, + }); })(), ).to.eventually.be.rejected; }); @@ -319,13 +333,13 @@ describe(Support.getTestDialectTeaser('Transaction'), () => { this.sequelize.startUnmanagedTransaction().then(async t => { await Promise.all([ expect(t.rollback()).to.eventually.be.fulfilled, - expect(this.sequelize.query('SELECT 1+1', { transaction: t, raw: true })) + expect(this.sequelize.query(`SELECT 1+1${fromQuery()}`, { transaction: t, raw: true })) .to.be.eventually.rejectedWith( Error, /rollback 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'), + .that.equal(`SELECT 1+1${fromQuery()}`), ]); }), ).to.eventually.be.fulfilled; diff --git a/packages/core/test/integration/utils.test.ts b/packages/core/test/integration/utils.test.ts index 095d5505614b..02637e78371c 100644 --- a/packages/core/test/integration/utils.test.ts +++ b/packages/core/test/integration/utils.test.ts @@ -35,7 +35,7 @@ describe(getTestDialectTeaser('fn()'), () => { // some dialects return the result of arithmetic functions (SUM, COUNT) as integer & floats, others as bigints & decimals. const arithmeticAsNumber = dialectName === 'sqlite3' || dialectName === 'db2'; - if (dialectName !== 'mssql' && dialectName !== 'ibmi') { + if (!['mssql', 'ibmi', 'oracle'].includes(dialectName)) { it('accepts condition object (with cast)', async () => { const type = dialectName === 'mysql' ? 'unsigned' : 'int'; @@ -82,7 +82,7 @@ describe(getTestDialectTeaser('fn()'), () => { }); } - if (dialectName !== 'mssql' && dialectName !== 'postgres' && dialectName !== 'ibmi') { + if (!['mssql', 'postgres', 'ibmi', 'oracle'].includes(dialectName)) { it('accepts condition object (auto casting)', async () => { const [airplane] = await vars.Airplane.findAll({ attributes: [ diff --git a/packages/core/test/unit/configuration.test.ts b/packages/core/test/unit/configuration.test.ts index 5aae50908c46..521961190714 100644 --- a/packages/core/test/unit/configuration.test.ts +++ b/packages/core/test/unit/configuration.test.ts @@ -28,7 +28,7 @@ describe('Sequelize constructor', () => { new Sequelize({ dialect: 'some-fancy-dialect' }); }).to.throw( Error, - 'The dialect some-fancy-dialect is not natively supported. Native dialects: mariadb, mssql, mysql, postgres, sqlite3, ibmi, db2 and snowflake.', + 'The dialect some-fancy-dialect is not natively supported. Native dialects: mariadb, mssql, mysql, postgres, sqlite3, ibmi, db2, oracle and snowflake.', ); }); diff --git a/packages/core/test/unit/data-types/binary-types.test.ts b/packages/core/test/unit/data-types/binary-types.test.ts index 1479c0bba120..c91f1611d9bb 100644 --- a/packages/core/test/unit/data-types/binary-types.test.ts +++ b/packages/core/test/unit/data-types/binary-types.test.ts @@ -17,6 +17,7 @@ describe('DataTypes.BLOB', () => { db2: 'BLOB(255)', postgres: 'BYTEA', sqlite3: 'BLOB', + oracle: 'BLOB', }); testDataTypeSql('BLOB("medium")', DataTypes.BLOB('medium'), { @@ -26,6 +27,7 @@ describe('DataTypes.BLOB', () => { db2: 'BLOB(16M)', postgres: 'BYTEA', sqlite3: 'BLOB', + oracle: 'BLOB', }); testDataTypeSql('BLOB({ length: "medium" })', DataTypes.BLOB({ length: 'medium' }), { @@ -35,6 +37,7 @@ describe('DataTypes.BLOB', () => { db2: 'BLOB(16M)', postgres: 'BYTEA', sqlite3: 'BLOB', + oracle: 'BLOB', }); testDataTypeSql('BLOB("long")', DataTypes.BLOB('long'), { @@ -44,6 +47,7 @@ describe('DataTypes.BLOB', () => { db2: 'BLOB(2G)', postgres: 'BYTEA', sqlite3: 'BLOB', + oracle: 'BLOB', }); describe('validate', () => { diff --git a/packages/core/test/unit/data-types/decimal-numbers.test.ts b/packages/core/test/unit/data-types/decimal-numbers.test.ts index 5396b37d5267..99e371dac74f 100644 --- a/packages/core/test/unit/data-types/decimal-numbers.test.ts +++ b/packages/core/test/unit/data-types/decimal-numbers.test.ts @@ -16,21 +16,25 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat testDataTypeSql('REAL', DataTypes.REAL, { default: 'REAL', + oracle: 'BINARY_DOUBLE', }); testDataTypeSql('REAL.UNSIGNED', DataTypes.REAL.UNSIGNED, { default: 'REAL UNSIGNED', 'sqlite3 snowflake ibmi db2 mssql postgres': 'REAL', + oracle: 'BINARY_DOUBLE', }); testDataTypeSql('REAL(11, 12)', DataTypes.REAL(11, 12), { default: 'REAL(11, 12)', 'sqlite3 snowflake ibmi db2 mssql postgres': 'REAL', + oracle: 'BINARY_DOUBLE', }); testDataTypeSql('REAL(11, 12).UNSIGNED', DataTypes.REAL(11, 12).UNSIGNED, { default: 'REAL(11, 12) UNSIGNED', 'sqlite3 snowflake ibmi db2 mssql postgres': 'REAL', + oracle: 'BINARY_DOUBLE', }); testDataTypeSql( @@ -39,6 +43,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat { default: 'REAL(11, 12) UNSIGNED', 'sqlite3 snowflake ibmi db2 mssql postgres': 'REAL', + oracle: 'BINARY_DOUBLE', }, ); @@ -68,6 +73,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'db2 ibmi': 'DOUBLE', sqlite3: 'REAL', snowflake: 'FLOAT', + oracle: 'BINARY_DOUBLE', }); testDataTypeSql('DOUBLE.UNSIGNED', DataTypes.DOUBLE.UNSIGNED, { @@ -76,6 +82,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'db2 ibmi': 'DOUBLE', 'postgres mssql': 'DOUBLE PRECISION', snowflake: 'FLOAT', + oracle: 'BINARY_DOUBLE', }); testDataTypeSql('DOUBLE(11, 12)', DataTypes.DOUBLE(11, 12), { @@ -84,6 +91,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'db2 ibmi': 'DOUBLE', 'postgres mssql': 'DOUBLE PRECISION', snowflake: 'FLOAT', + oracle: 'BINARY_DOUBLE', }); testDataTypeSql('DOUBLE(11, 12).UNSIGNED', DataTypes.DOUBLE(11, 12).UNSIGNED, { @@ -92,6 +100,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'db2 ibmi': 'DOUBLE', 'postgres mssql': 'DOUBLE PRECISION', snowflake: 'FLOAT', + oracle: 'BINARY_DOUBLE', }); testDataTypeSql('DOUBLE(11, 12).UNSIGNED.ZEROFILL', DataTypes.DOUBLE(11, 12).UNSIGNED.ZEROFILL, { @@ -134,24 +143,28 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'mysql mariadb snowflake': 'FLOAT', // REAL in sqlite is double-precision (no single-precision support), but single-precision in all others 'postgres mssql sqlite3 db2 ibmi': 'REAL', + oracle: 'BINARY_FLOAT', }); testDataTypeSql('FLOAT.UNSIGNED', DataTypes.FLOAT.UNSIGNED, { 'mysql mariadb': 'FLOAT UNSIGNED', snowflake: 'FLOAT', 'postgres mssql sqlite3 db2 ibmi': 'REAL', + oracle: 'BINARY_FLOAT', }); testDataTypeSql('FLOAT(11, 12)', DataTypes.FLOAT(11, 12), { 'mysql mariadb': 'FLOAT(11, 12)', snowflake: 'FLOAT', 'postgres mssql sqlite3 db2 ibmi': 'REAL', + oracle: 'BINARY_FLOAT', }); testDataTypeSql('FLOAT(11, 12).UNSIGNED', DataTypes.FLOAT(11, 12).UNSIGNED, { 'mysql mariadb': 'FLOAT(11, 12) UNSIGNED', snowflake: 'FLOAT', 'postgres mssql sqlite3 db2 ibmi': 'REAL', + oracle: 'BINARY_FLOAT', }); testDataTypeSql( @@ -161,6 +174,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'mysql mariadb': 'FLOAT(11, 12) UNSIGNED', snowflake: 'FLOAT', 'postgres mssql sqlite3 db2 ibmi': 'REAL', + oracle: 'BINARY_FLOAT', }, ); @@ -225,11 +239,13 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat ), sqlite3: unsupportedError, postgres: 'DECIMAL', + oracle: 'NUMBER', }); testDataTypeSql('DECIMAL(10, 2)', DataTypes.DECIMAL(10, 2), { default: 'DECIMAL(10, 2)', sqlite3: unsupportedError, + oracle: 'NUMBER(10, 2)', }); testDataTypeSql( @@ -238,6 +254,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat { default: 'DECIMAL(10, 2)', sqlite3: unsupportedError, + oracle: 'NUMBER(10, 2)', }, ); @@ -245,6 +262,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat default: 'DECIMAL(10, 2)', 'mysql mariadb': 'DECIMAL(10, 2) UNSIGNED', sqlite3: unsupportedError, + oracle: 'NUMBER(10, 2)', }); testDataTypeSql('DECIMAL(10, 2).UNSIGNED.ZEROFILL', DataTypes.DECIMAL(10, 2).UNSIGNED.ZEROFILL, { @@ -260,6 +278,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat default: 'DECIMAL(10, 2)', 'mysql mariadb': 'DECIMAL(10, 2) UNSIGNED', sqlite3: unsupportedError, + oracle: 'NUMBER(10, 2)', }, ); @@ -283,7 +302,10 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat it('should throw an error if `value` is invalid', () => { const type: DataTypeInstance = DataTypes.DECIMAL(10, 2).toDialectDataType(dialect); - const typeName = supportsDecimal.constrained ? 'decimal(10, 2)' : 'decimal'; + let typeName = supportsDecimal.constrained ? 'decimal(10, 2)' : 'decimal'; + if (dialect.name === 'oracle') { + typeName = 'number(10, 2)'; + } expect(() => { type.validate('foobar'); diff --git a/packages/core/test/unit/data-types/integers.test.ts b/packages/core/test/unit/data-types/integers.test.ts index a7663e4dd386..455a7786b49f 100644 --- a/packages/core/test/unit/data-types/integers.test.ts +++ b/packages/core/test/unit/data-types/integers.test.ts @@ -21,6 +21,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'mssql postgres db2 ibmi': 'SMALLINT', 'mysql mariadb': 'TINYINT', 'sqlite3 snowflake': 'INTEGER', + oracle: 'NUMBER(3)', }, }, { @@ -31,6 +32,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'mssql postgres db2 ibmi': 'SMALLINT', 'mysql mariadb': 'TINYINT(2)', 'sqlite3 snowflake': 'INTEGER', + oracle: 'NUMBER(3)', }, }, { @@ -40,6 +42,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'mssql postgres db2 ibmi': 'SMALLINT', 'mysql mariadb': 'TINYINT(2)', 'sqlite3 snowflake': 'INTEGER', + oracle: 'NUMBER(3)', }, }, { @@ -53,6 +56,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'sqlite3 snowflake': 'INTEGER', // TINYINT is unsigned in mssql mssql: 'TINYINT', + oracle: 'NUMBER(3)', }, }, { @@ -63,6 +67,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'mysql mariadb': 'TINYINT(2) UNSIGNED', 'sqlite3 snowflake': 'INTEGER', mssql: 'TINYINT', + oracle: 'NUMBER(3)', }, }, { @@ -164,6 +169,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat default: 'SMALLINT', 'sqlite3 snowflake': 'INTEGER', 'mysql mariadb': 'SMALLINT(4)', + oracle: 'NUMBER(4,0)', }, }, { @@ -173,6 +179,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat default: 'SMALLINT', 'sqlite3 snowflake': 'INTEGER', 'mysql mariadb': 'SMALLINT(4)', + oracle: 'NUMBER(4,0)', }, }, { @@ -184,6 +191,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'sqlite3 snowflake': 'INTEGER', 'postgres db2 ibmi': 'INTEGER', mssql: 'INT', + oracle: 'SMALLINT', }, }, { @@ -194,6 +202,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'sqlite3 snowflake': 'INTEGER', 'postgres db2 ibmi': 'INTEGER', mssql: 'INT', + oracle: 'NUMBER(4,0)', }, }, { @@ -287,6 +296,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'mariadb mysql': 'MEDIUMINT', // falls back to larger type + CHECK constraint 'db2 ibmi mssql postgres snowflake sqlite3': 'INTEGER', + oracle: 'NUMBER(8)', }, }, { @@ -295,6 +305,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat expect: { 'mariadb mysql': 'MEDIUMINT(2)', 'db2 ibmi mssql postgres snowflake sqlite3': 'INTEGER', + oracle: 'NUMBER(8)', }, }, { @@ -303,6 +314,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat expect: { 'mariadb mysql': 'MEDIUMINT(2)', 'db2 ibmi mssql postgres snowflake sqlite3': 'INTEGER', + oracle: 'NUMBER(8)', }, }, { @@ -311,6 +323,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat expect: { 'mariadb mysql': 'MEDIUMINT UNSIGNED', 'db2 ibmi mssql postgres snowflake sqlite3': 'INTEGER', + oracle: 'NUMBER(8)', }, }, { @@ -319,6 +332,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat expect: { 'mariadb mysql': 'MEDIUMINT(2) UNSIGNED', 'db2 ibmi mssql postgres snowflake sqlite3': 'INTEGER', + oracle: 'NUMBER(8)', }, }, { @@ -410,7 +424,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat testDataTypeSql('INTEGER.UNSIGNED', DataTypes.INTEGER.UNSIGNED, { // sqlite & snowflake are both 64 bits integers (actually snowflake accepts up to 99999999999999999999999999999999999999) - 'sqlite3 snowflake': 'INTEGER', + 'sqlite3 oracle snowflake': 'INTEGER', 'mysql mariadb': 'INTEGER UNSIGNED', 'ibmi postgres db2 mssql': 'BIGINT', }); @@ -423,17 +437,20 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat testDataTypeSql('INTEGER(11)', DataTypes.INTEGER(11), { default: 'INTEGER', 'mysql mariadb': 'INTEGER(11)', + oracle: 'NUMBER(11,0)', }); testDataTypeSql('INTEGER({ length: 11 })', DataTypes.INTEGER({ length: 11 }), { default: 'INTEGER', 'mysql mariadb': 'INTEGER(11)', + oracle: 'NUMBER(11,0)', }); testDataTypeSql('INTEGER(11).UNSIGNED', DataTypes.INTEGER(11).UNSIGNED, { 'mysql mariadb': 'INTEGER(11) UNSIGNED', 'sqlite3 snowflake': 'INTEGER', 'ibmi postgres db2 mssql': 'BIGINT', + oracle: 'NUMBER(11,0)', }); testDataTypeSql('INTEGER(11).UNSIGNED.ZEROFILL', DataTypes.INTEGER(11).UNSIGNED.ZEROFILL, { @@ -492,6 +509,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat testDataTypeSql('BIGINT', DataTypes.BIGINT, { default: 'BIGINT', 'sqlite3 snowflake': 'INTEGER', + oracle: 'NUMBER(19, 0)', }); testDataTypeSql('BIGINT.UNSIGNED', DataTypes.BIGINT.UNSIGNED, { @@ -499,6 +517,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'mysql mariadb': 'BIGINT UNSIGNED', // INTEGER in snowflake goes up to 99999999999999999999999999999999999999, which is enough to store an unsigned 64-bit integer. snowflake: 'INTEGER', + oracle: 'NUMBER(19, 0)', }); testDataTypeSql('BIGINT.UNSIGNED.ZEROFILL', DataTypes.BIGINT.UNSIGNED.ZEROFILL, { @@ -510,12 +529,14 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat default: 'BIGINT', 'sqlite3 snowflake': 'INTEGER', 'mysql mariadb': 'BIGINT(11)', + oracle: 'NUMBER(19, 0)', }); testDataTypeSql('BIGINT({ length: 11 })', DataTypes.BIGINT({ length: 11 }), { default: 'BIGINT', 'sqlite3 snowflake': 'INTEGER', 'mysql mariadb': 'BIGINT(11)', + oracle: 'NUMBER(19, 0)', }); testDataTypeSql('BIGINT(11).UNSIGNED', DataTypes.BIGINT(11).UNSIGNED, { @@ -523,6 +544,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat default: unsignedUnsupportedError, 'mysql mariadb': 'BIGINT(11) UNSIGNED', snowflake: 'INTEGER', + oracle: 'NUMBER(19, 0)', }); testDataTypeSql('BIGINT(11).UNSIGNED.ZEROFILL', DataTypes.BIGINT(11).UNSIGNED.ZEROFILL, { diff --git a/packages/core/test/unit/data-types/misc-data-types.test.ts b/packages/core/test/unit/data-types/misc-data-types.test.ts index 3ea3c343895b..5c1335225889 100644 --- a/packages/core/test/unit/data-types/misc-data-types.test.ts +++ b/packages/core/test/unit/data-types/misc-data-types.test.ts @@ -17,6 +17,7 @@ describe('DataTypes.BOOLEAN', () => { mariadb: 'TINYINT(1)', mysql: 'TINYINT(1)', sqlite3: 'INTEGER', + oracle: 'CHAR(1)', }); describe('validate', () => { @@ -57,6 +58,7 @@ describe('DataTypes.ENUM', () => { mssql: `NVARCHAR(255)`, sqlite3: 'TEXT', 'db2 ibmi snowflake': 'VARCHAR(255)', + oracle: 'VARCHAR2(512)', }); }); @@ -188,6 +190,7 @@ describe('DataTypes.JSON', () => { // SQL server supports JSON functions, but it is stored as a string with a ISJSON constraint. mssql: 'NVARCHAR(MAX)', sqlite3: 'TEXT', + oracle: 'BLOB', }); describe('escape', () => { @@ -200,6 +203,7 @@ describe('DataTypes.JSON', () => { default: `'"string"'`, mysql: `CAST('"string"' AS JSON)`, mssql: `N'"string"'`, + oracle: `'string'`, }); }); diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index e4df01fa7f28..5279749b8fe1 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -17,18 +17,21 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat default: 'VARCHAR(255)', mssql: 'NVARCHAR(255)', sqlite3: 'TEXT', + oracle: 'NVARCHAR2(255)', }); testDataTypeSql('STRING(1234)', DataTypes.STRING(1234), { default: 'VARCHAR(1234)', mssql: 'NVARCHAR(1234)', sqlite3: 'TEXT', + oracle: 'NVARCHAR2(1234)', }); testDataTypeSql('STRING({ length: 1234 })', DataTypes.STRING({ length: 1234 }), { default: 'VARCHAR(1234)', mssql: 'NVARCHAR(1234)', sqlite3: 'TEXT', + oracle: 'NVARCHAR2(1234)', }); testDataTypeSql('STRING(1234).BINARY', DataTypes.STRING(1234).BINARY, { @@ -36,6 +39,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'db2 ibmi': 'VARCHAR(1234) FOR BIT DATA', sqlite3: 'TEXT COLLATE BINARY', 'mssql postgres': binaryCollationUnsupportedError, + oracle: 'RAW(1234)', }); testDataTypeSql('STRING.BINARY', DataTypes.STRING.BINARY, { @@ -43,6 +47,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'db2 ibmi': 'VARCHAR(255) FOR BIT DATA', sqlite3: 'TEXT COLLATE BINARY', 'mssql postgres': binaryCollationUnsupportedError, + oracle: 'RAW(255)', }); }); @@ -62,6 +67,7 @@ describe('DataTypes.TEXT', () => { default: 'TEXT', 'ibmi db2': 'CLOB(2147483647)', mssql: 'NVARCHAR(MAX)', // in mssql text is actually representing a non unicode text field + oracle: 'CLOB', }); testDataTypeSql('TEXT("tiny")', DataTypes.TEXT('tiny'), { @@ -69,6 +75,7 @@ describe('DataTypes.TEXT', () => { 'ibmi db2': 'VARCHAR(256)', mssql: 'NVARCHAR(256)', 'mariadb mysql': 'TINYTEXT', + oracle: 'CLOB', }); testDataTypeSql('TEXT({ length: "tiny" })', DataTypes.TEXT({ length: 'tiny' }), { @@ -76,6 +83,7 @@ describe('DataTypes.TEXT', () => { 'ibmi db2': 'VARCHAR(256)', mssql: 'NVARCHAR(256)', 'mariadb mysql': 'TINYTEXT', + oracle: 'CLOB', }); testDataTypeSql('TEXT("medium")', DataTypes.TEXT('medium'), { @@ -83,6 +91,7 @@ describe('DataTypes.TEXT', () => { 'ibmi db2': 'CLOB(16777216)', mssql: 'NVARCHAR(MAX)', 'mariadb mysql': 'MEDIUMTEXT', + oracle: 'CLOB', }); testDataTypeSql('TEXT("long")', DataTypes.TEXT('long'), { @@ -90,6 +99,7 @@ describe('DataTypes.TEXT', () => { 'ibmi db2': 'CLOB(2147483647)', mssql: 'NVARCHAR(MAX)', 'mariadb mysql': 'LONGTEXT', + oracle: 'CLOB', }); }); @@ -166,6 +176,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'db2 ibmi': 'CHAR(12) FOR BIT DATA', sqlite3: charNotSupportedError, 'postgres mssql': binaryNotSupportedError, + oracle: 'RAW(12)', }); testDataTypeSql('CHAR.BINARY', DataTypes.CHAR.BINARY, { @@ -173,6 +184,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat 'db2 ibmi': 'CHAR(255) FOR BIT DATA', sqlite3: charNotSupportedError, 'postgres mssql': binaryNotSupportedError, + oracle: 'RAW(255)', }); }); }); diff --git a/packages/core/test/unit/data-types/temporal-types.test.ts b/packages/core/test/unit/data-types/temporal-types.test.ts index e00cd34f144f..f543f49021cd 100644 --- a/packages/core/test/unit/data-types/temporal-types.test.ts +++ b/packages/core/test/unit/data-types/temporal-types.test.ts @@ -17,6 +17,7 @@ describe('DataTypes.DATE', () => { mssql: 'DATETIMEOFFSET', 'mariadb mysql': 'DATETIME', sqlite3: 'TEXT', + oracle: 'TIMESTAMP WITH LOCAL TIME ZONE', }); testDataTypeSql('DATE(0)', DataTypes.DATE(0), { @@ -25,6 +26,7 @@ describe('DataTypes.DATE', () => { 'mariadb mysql': 'DATETIME(0)', 'db2 ibmi snowflake': 'TIMESTAMP(0)', sqlite3: 'TEXT', + oracle: 'TIMESTAMP WITH LOCAL TIME ZONE', }); testDataTypeSql('DATE(6)', DataTypes.DATE(6), { @@ -34,6 +36,7 @@ describe('DataTypes.DATE', () => { mariadb: 'DATETIME(6)', mysql: 'DATETIME(6)', sqlite3: 'TEXT', + oracle: 'TIMESTAMP WITH LOCAL TIME ZONE', }); }); @@ -137,6 +140,8 @@ describe('DataTypes.TIME', () => { db2: new Error(`db2 does not support the TIME(precision) data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), sqlite3: 'TEXT', + oracle: new Error(`oracle does not support the TIME(precision) data type. +See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types`), }); }); }); @@ -147,6 +152,7 @@ describe('DataTypes.NOW', () => { default: 'NOW', db2: 'CURRENT TIME', mssql: 'GETDATE()', + oracle: 'SYSDATE', }); }); }); diff --git a/packages/core/test/unit/data-types/uuid.test.ts b/packages/core/test/unit/data-types/uuid.test.ts index fdf416abe350..f306f010b7f6 100644 --- a/packages/core/test/unit/data-types/uuid.test.ts +++ b/packages/core/test/unit/data-types/uuid.test.ts @@ -15,6 +15,7 @@ describe('DataTypes.UUID', () => { 'mariadb mysql': 'CHAR(36) BINARY', snowflake: 'VARCHAR(36)', sqlite3: 'TEXT', + oracle: 'VARCHAR2(36)', }); }); diff --git a/packages/core/test/unit/pool.test.ts b/packages/core/test/unit/pool.test.ts index 99b2584c9796..9d79cad882df 100644 --- a/packages/core/test/unit/pool.test.ts +++ b/packages/core/test/unit/pool.test.ts @@ -132,6 +132,9 @@ describe('sequelize.pool', () => { snowflake: { account: 'replica1', }, + oracle: { + host: 'replica1', + }, }; const replica2Overrides: DialectConnectionConfigs = { @@ -159,6 +162,9 @@ describe('sequelize.pool', () => { snowflake: { account: 'replica2', }, + oracle: { + host: 'replica2', + }, }; const connectionOptions = sequelize.options.replication.write; @@ -230,6 +236,9 @@ describe('sequelize.pool', () => { snowflake: { account: 'write', }, + oracle: { + host: 'write', + }, }; const connectionOptions = sequelize.options.replication.write; diff --git a/packages/core/test/unit/query-generator/add-column-query.test.ts b/packages/core/test/unit/query-generator/add-column-query.test.ts index ece57cdcfe1c..ea980a43a357 100644 --- a/packages/core/test/unit/query-generator/add-column-query.test.ts +++ b/packages/core/test/unit/query-generator/add-column-query.test.ts @@ -31,6 +31,7 @@ describe('QueryGenerator#addColumnQuery', () => { default: `ALTER TABLE [Users] ADD [age] INTEGER;`, mssql: `ALTER TABLE [Users] ADD [age] INTEGER NULL;`, postgres: `ALTER TABLE "Users" ADD COLUMN "age" INTEGER;`, + oracle: `ALTER TABLE "Users" ADD "age" INTEGER NULL;`, }, ); }); diff --git a/packages/core/test/unit/query-generator/add-constraint-query.test.ts b/packages/core/test/unit/query-generator/add-constraint-query.test.ts index 492431e7a0f0..244a088fa168 100644 --- a/packages/core/test/unit/query-generator/add-constraint-query.test.ts +++ b/packages/core/test/unit/query-generator/add-constraint-query.test.ts @@ -561,7 +561,7 @@ describe('QueryGenerator#addConstraintQuery', () => { { default: `ALTER TABLE [myTable] ADD CONSTRAINT [myTable_otherId_otherTable_fk] FOREIGN KEY ([otherId]) REFERENCES [otherTable] ([id]) ON UPDATE CASCADE`, sqlite3: notSupportedError, - 'db2 ibmi': onUpdateNotSupportedError, + 'db2 ibmi oracle': onUpdateNotSupportedError, }, ); }); diff --git a/packages/core/test/unit/query-generator/bulk-delete-query.test.ts b/packages/core/test/unit/query-generator/bulk-delete-query.test.ts index ca2003457840..60e06e6d9f58 100644 --- a/packages/core/test/unit/query-generator/bulk-delete-query.test.ts +++ b/packages/core/test/unit/query-generator/bulk-delete-query.test.ts @@ -24,7 +24,7 @@ describe('QueryGenerator#bulkDeleteQuery', () => { sqlite3: "DELETE FROM `myTable` WHERE rowid IN (SELECT rowid FROM `myTable` WHERE `name` = 'barry' LIMIT 10)", 'db2 ibmi': `DELETE FROM "myTable" WHERE "name" = 'barry' FETCH NEXT 10 ROWS ONLY`, - 'mssql postgres snowflake': limitNotSupportedError, + 'mssql postgres snowflake oracle': limitNotSupportedError, }, ); }); @@ -39,6 +39,7 @@ describe('QueryGenerator#bulkDeleteQuery', () => { "DELETE FROM `MyModels` WHERE rowid IN (SELECT rowid FROM `MyModels` WHERE `name` = 'barry' LIMIT 10)", 'db2 ibmi': `DELETE FROM "MyModels" WHERE "name" = 'barry' FETCH NEXT 10 ROWS ONLY`, 'postgres snowflake': `DELETE FROM "MyModels" WHERE "id" IN (SELECT "id" FROM "MyModels" WHERE "name" = 'barry' ORDER BY "id" LIMIT 10)`, + oracle: `DELETE FROM "MyModels" WHERE rowid IN (SELECT rowid FROM "MyModels" WHERE rownum <= 10 AND "name" = 'barry')`, }); }); @@ -55,6 +56,7 @@ describe('QueryGenerator#bulkDeleteQuery', () => { "DELETE FROM `MyModels` WHERE rowid IN (SELECT rowid FROM `MyModels` WHERE `name` = 'barry' LIMIT 10)", 'db2 ibmi': `DELETE FROM "MyModels" WHERE "name" = 'barry' FETCH NEXT 10 ROWS ONLY`, 'postgres snowflake': `DELETE FROM "MyModels" WHERE "id" IN (SELECT "id" FROM "MyModels" WHERE "name" = 'barry' ORDER BY "id" LIMIT 10)`, + oracle: `DELETE FROM "MyModels" WHERE rowid IN (SELECT rowid FROM "MyModels" WHERE rownum <= 10 AND "name" = 'barry')`, }, ); }); @@ -78,6 +80,7 @@ describe('QueryGenerator#bulkDeleteQuery', () => { sqlite3: `DELETE FROM \`MyModels\` WHERE rowid IN (SELECT rowid FROM \`MyModels\` WHERE name = 'Zoe' LIMIT 1)`, 'db2 ibmi': `DELETE FROM "MyModels" WHERE name = 'Zoe' FETCH NEXT 1 ROWS ONLY`, 'postgres snowflake': `DELETE FROM "MyModels" WHERE "id" IN (SELECT "id" FROM "MyModels" WHERE name = 'Zoe' ORDER BY "id" LIMIT 1)`, + oracle: `DELETE FROM "MyModels" WHERE rowid IN (SELECT rowid FROM "MyModels" WHERE rownum <= :limit AND name = 'Zoe')`, }); }); diff --git a/packages/core/test/unit/query-generator/bulk-insert-query.test.ts b/packages/core/test/unit/query-generator/bulk-insert-query.test.ts index 507e1a52ea11..2abdbe7ad50e 100644 --- a/packages/core/test/unit/query-generator/bulk-insert-query.test.ts +++ b/packages/core/test/unit/query-generator/bulk-insert-query.test.ts @@ -1,5 +1,7 @@ import { DataTypes, literal } from '@sequelize/core'; -import { beforeAll2, expectsql, sequelize } from '../../support'; +import { beforeAll2, expectsql, getTestDialect, sequelize } from '../../support'; + +const dialect = getTestDialect(); describe('QueryGenerator#bulkInsertQuery', () => { const queryGenerator = sequelize.queryGenerator; @@ -17,6 +19,11 @@ describe('QueryGenerator#bulkInsertQuery', () => { }); it('parses named replacements in literals', async () => { + // The Oracle dialect doesn't support replacements for bulkInsert + if (dialect === 'oracle') { + return; + } + const { User } = vars; const sql = queryGenerator.bulkInsertQuery( diff --git a/packages/core/test/unit/query-generator/commit-transaction-query.test.ts b/packages/core/test/unit/query-generator/commit-transaction-query.test.ts index b707b6c9f9c5..dc12c2ea438d 100644 --- a/packages/core/test/unit/query-generator/commit-transaction-query.test.ts +++ b/packages/core/test/unit/query-generator/commit-transaction-query.test.ts @@ -12,6 +12,7 @@ describe('QueryGenerator#commitTransactionQuery', () => { expectsql(() => queryGenerator.commitTransactionQuery(), { default: 'COMMIT', 'db2 ibmi mssql': notSupportedError, + oracle: 'COMMIT TRANSACTION', }); }); }); diff --git a/packages/core/test/unit/query-generator/create-schema-query.test.ts b/packages/core/test/unit/query-generator/create-schema-query.test.ts index ac624615e5b2..4880e9f3cf01 100644 --- a/packages/core/test/unit/query-generator/create-schema-query.test.ts +++ b/packages/core/test/unit/query-generator/create-schema-query.test.ts @@ -13,6 +13,7 @@ describe('QueryGenerator#createSchemaQuery', () => { expectsql(() => queryGenerator.createSchemaQuery('mySchema'), { default: 'CREATE SCHEMA [mySchema]', sqlite3: notSupportedError, + oracle: `DECLARE USER_FOUND BOOLEAN := FALSE; BEGIN BEGIN EXECUTE IMMEDIATE 'CREATE USER "mySchema" IDENTIFIED BY 12345 DEFAULT TABLESPACE USERS' ; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -1920 THEN RAISE; ELSE USER_FOUND := TRUE; END IF; END; IF NOT USER_FOUND THEN EXECUTE IMMEDIATE 'GRANT "CONNECT" TO "mySchema"' ; EXECUTE IMMEDIATE 'GRANT CREATE TABLE TO "mySchema"' ; EXECUTE IMMEDIATE 'GRANT CREATE VIEW TO "mySchema"' ; EXECUTE IMMEDIATE 'GRANT CREATE ANY TRIGGER TO "mySchema"' ; EXECUTE IMMEDIATE 'GRANT CREATE ANY PROCEDURE TO "mySchema"' ; EXECUTE IMMEDIATE 'GRANT CREATE SEQUENCE TO "mySchema"' ; EXECUTE IMMEDIATE 'GRANT CREATE SYNONYM TO "mySchema"' ; EXECUTE IMMEDIATE 'ALTER USER "mySchema" QUOTA UNLIMITED ON USERS' ; END IF; END;`, }); }); @@ -20,9 +21,11 @@ describe('QueryGenerator#createSchemaQuery', () => { expectsql(() => queryGenerator.createSchemaQuery('mySchema', { authorization: 'myUser' }), { default: 'CREATE SCHEMA [mySchema] AUTHORIZATION [myUser]', sqlite3: notSupportedError, - 'mariadb mysql snowflake': buildInvalidOptionReceivedError('createSchemaQuery', dialectName, [ - 'authorization', - ]), + 'mariadb mysql snowflake oracle': buildInvalidOptionReceivedError( + 'createSchemaQuery', + dialectName, + ['authorization'], + ), }); }); @@ -32,7 +35,7 @@ describe('QueryGenerator#createSchemaQuery', () => { { default: 'CREATE SCHEMA [mySchema] AUTHORIZATION CURRENT USER', sqlite3: notSupportedError, - 'mariadb mysql snowflake': buildInvalidOptionReceivedError( + 'mariadb mysql snowflake oracle': buildInvalidOptionReceivedError( 'createSchemaQuery', dialectName, ['authorization'], @@ -68,7 +71,7 @@ describe('QueryGenerator#createSchemaQuery', () => { it('supports the ifNotExists option', () => { expectsql(() => queryGenerator.createSchemaQuery('mySchema', { ifNotExists: true }), { default: 'CREATE SCHEMA IF NOT EXISTS [mySchema]', - 'db2 ibmi mssql': buildInvalidOptionReceivedError('createSchemaQuery', dialectName, [ + 'db2 ibmi mssql oracle': buildInvalidOptionReceivedError('createSchemaQuery', dialectName, [ 'ifNotExists', ]), sqlite3: notSupportedError, @@ -122,6 +125,11 @@ describe('QueryGenerator#createSchemaQuery', () => { 'charset', 'collate', ]), + oracle: buildInvalidOptionReceivedError('createSchemaQuery', dialectName, [ + 'authorization', + 'charset', + 'collate', + ]), sqlite3: notSupportedError, }, ); diff --git a/packages/core/test/unit/query-generator/create-table-query.test.ts b/packages/core/test/unit/query-generator/create-table-query.test.ts index 5df2e914feec..d10808bd8023 100644 --- a/packages/core/test/unit/query-generator/create-table-query.test.ts +++ b/packages/core/test/unit/query-generator/create-table-query.test.ts @@ -17,6 +17,7 @@ describe('QueryGenerator#createTableQuery', () => { 'mariadb mysql': 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` DATE) ENGINE=InnoDB;', mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] DATE);`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); @@ -28,6 +29,7 @@ describe('QueryGenerator#createTableQuery', () => { 'mariadb mysql': 'CREATE TABLE IF NOT EXISTS `MyModels` (`myColumn` DATE) ENGINE=InnoDB;', mssql: `IF OBJECT_ID(N'[MyModels]', 'U') IS NULL CREATE TABLE [MyModels] ([myColumn] DATE);`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "MyModels" ("myColumn" DATE); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "MyModels" ("myColumn" DATE)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); @@ -40,6 +42,7 @@ describe('QueryGenerator#createTableQuery', () => { 'mariadb mysql': 'CREATE TABLE IF NOT EXISTS `MyModels` (`myColumn` DATE) ENGINE=InnoDB;', mssql: `IF OBJECT_ID(N'[MyModels]', 'U') IS NULL CREATE TABLE [MyModels] ([myColumn] DATE);`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "MyModels" ("myColumn" DATE); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "MyModels" ("myColumn" DATE)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); @@ -56,6 +59,7 @@ describe('QueryGenerator#createTableQuery', () => { mssql: `IF OBJECT_ID(N'[mySchema].[myTable]', 'U') IS NULL CREATE TABLE [mySchema].[myTable] ([myColumn] DATE);`, sqlite3: 'CREATE TABLE IF NOT EXISTS `mySchema.myTable` (`myColumn` DATE);', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "mySchema"."myTable" ("myColumn" DATE); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "mySchema"."myTable" ("myColumn" DATE)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -71,6 +75,7 @@ describe('QueryGenerator#createTableQuery', () => { 'mariadb mysql': 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` DATE) ENGINE=InnoDB;', mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] DATE);`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -86,6 +91,7 @@ describe('QueryGenerator#createTableQuery', () => { mssql: `IF OBJECT_ID(N'[mySchema].[myTable]', 'U') IS NULL CREATE TABLE [mySchema].[myTable] ([myColumn] DATE);`, sqlite3: 'CREATE TABLE IF NOT EXISTS `mySchema.myTable` (`myColumn` DATE);', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "mySchema"."myTable" ("myColumn" DATE); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "mySchema"."myTable" ("myColumn" DATE)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); @@ -115,6 +121,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` DATE, `secondColumn` TEXT) ENGINE=InnoDB;', mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] DATE, [secondColumn] TEXT);`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE, "secondColumn" TEXT); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE, "secondColumn" TEXT)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -127,6 +134,7 @@ describe('QueryGenerator#createTableQuery', () => { mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] DATE, PRIMARY KEY ([myColumn]));`, sqlite3: 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` DATE PRIMARY KEY);', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE, PRIMARY KEY ("myColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE,PRIMARY KEY ("myColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); @@ -145,6 +153,7 @@ describe('QueryGenerator#createTableQuery', () => { sqlite3: 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` DATE NOT NULL, `secondColumn` TEXT NOT NULL, PRIMARY KEY (`myColumn`, `secondColumn`));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE, "secondColumn" TEXT, PRIMARY KEY ("myColumn", "secondColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE, "secondColumn" TEXT,PRIMARY KEY ("myColumn", "secondColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -161,6 +170,7 @@ describe('QueryGenerator#createTableQuery', () => { 'snowflake db2': 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE, FOREIGN KEY ("myColumn") REFERENCES "Bar" ("id"));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE REFERENCES "Bar" ("id")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE,FOREIGN KEY ("myColumn") REFERENCES "Bar" ("id"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -181,6 +191,7 @@ describe('QueryGenerator#createTableQuery', () => { 'snowflake db2': 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE, PRIMARY KEY ("myColumn"), FOREIGN KEY ("myColumn") REFERENCES "Bar" ("id"));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE REFERENCES "Bar" ("id"), PRIMARY KEY ("myColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE,PRIMARY KEY ("myColumn"),FOREIGN KEY ("myColumn") REFERENCES "Bar" ("id"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -205,6 +216,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE, FOREIGN KEY ("myColumn") REFERENCES "Bar" ("id") COMMENT Foo);', db2: `CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE, FOREIGN KEY ("myColumn") REFERENCES "Bar" ("id")); -- 'Foo', TableName = "myTable", ColumnName = "myColumn";`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE REFERENCES "Bar" ("id") COMMENT Foo); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE,FOREIGN KEY ("myColumn") REFERENCES "Bar" ("id") COMMENT Foo)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -216,6 +228,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` DATE NOT NULL) ENGINE=InnoDB;', mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] DATE NOT NULL);`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE NOT NULL); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE NOT NULL)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); @@ -236,6 +249,7 @@ describe('QueryGenerator#createTableQuery', () => { sqlite3: 'CREATE TABLE IF NOT EXISTS `mySchema.myTable` (`myColumn` DATE COMMENT Foo);', db2: `CREATE TABLE IF NOT EXISTS "mySchema"."myTable" ("myColumn" DATE); -- 'Foo', TableName = "mySchema"."myTable", ColumnName = "myColumn";`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "mySchema"."myTable" ("myColumn" DATE COMMENT Foo); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "mySchema"."myTable" ("myColumn" DATE COMMENT Foo)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -259,6 +273,7 @@ describe('QueryGenerator#createTableQuery', () => { @level1type = N'Table', @level1name = [myTable], @level2type = N'Column', @level2name = [secondColumn];`, db2: `CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE, "secondColumn" DATE); -- 'Foo', TableName = "myTable", ColumnName = "myColumn"; -- 'Foo Bar', TableName = "myTable", ColumnName = "secondColumn";`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE COMMENT Foo, "secondColumn" DATE COMMENT Foo Bar); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE COMMENT Foo, "secondColumn" DATE COMMENT Foo Bar)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -277,6 +292,7 @@ describe('QueryGenerator#createTableQuery', () => { @level1type = N'Table', @level1name = [myTable], @level2type = N'Column', @level2name = [myColumn];`, db2: `CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE COMMENT Foo); -- 'Bar', TableName = "myTable", ColumnName = "myColumn";`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE COMMENT Foo COMMENT Bar); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE COMMENT Foo COMMENT Bar)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -296,6 +312,7 @@ describe('QueryGenerator#createTableQuery', () => { sqlite3: 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` DATE COMMENT Foo PRIMARY KEY);', db2: `CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE); -- 'Foo PRIMARY KEY', TableName = "myTable", ColumnName = "myColumn";`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE COMMENT Foo, PRIMARY KEY ("myColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE COMMENT Foo,PRIMARY KEY ("myColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -328,6 +345,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" "public"."enum_myTable_myColumn");', mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] ENUM("foo", "bar"));`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" ENUM("foo", "bar")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" ENUM("foo", "bar"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); @@ -345,6 +363,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` INTEGER, `secondColumn` BIGINT, `thirdColumn` SMALLINT) ENGINE=InnoDB;', mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] INTEGER, [secondColumn] BIGINT, [thirdColumn] SMALLINT);`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" INTEGER, "secondColumn" BIGINT, "thirdColumn" SMALLINT); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" INTEGER, "secondColumn" BIGINT, "thirdColumn" SMALLINT)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -365,6 +384,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" SERIAL, "secondColumn" BIGSERIAL, "thirdColumn" SMALLSERIAL);', mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] INTEGER SERIAL, [secondColumn] BIGINT SERIAL, [thirdColumn] SMALLINT SERIAL);`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" INTEGER SERIAL, "secondColumn" BIGINT SERIAL, "thirdColumn" SMALLINT SERIAL); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" INTEGER SERIAL, "secondColumn" BIGINT SERIAL, "thirdColumn" SMALLINT SERIAL)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -377,6 +397,7 @@ describe('QueryGenerator#createTableQuery', () => { postgres: 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" SERIAL);', mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] INTEGER SERIAL NOT NULL);`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" INTEGER SERIAL NOT NULL); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" INTEGER SERIAL NOT NULL)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); @@ -387,6 +408,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` INTEGER AUTOINCREMENT) ENGINE=InnoDB;', mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] INTEGER AUTOINCREMENT);`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" INTEGER AUTOINCREMENT); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" INTEGER AUTOINCREMENT)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, snowflake: 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" INTEGER DEFAULT "myTable_myColumn_seq".NEXTVAL);', }); @@ -401,6 +423,7 @@ describe('QueryGenerator#createTableQuery', () => { mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] INTEGER, PRIMARY KEY ([myColumn]));`, sqlite3: 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` INTEGER PRIMARY KEY);', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" INTEGER, PRIMARY KEY ("myColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" INTEGER,PRIMARY KEY ("myColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); @@ -419,6 +442,7 @@ describe('QueryGenerator#createTableQuery', () => { sqlite3: 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` INTEGER NOT NULL, `secondColumn` TEXT NOT NULL, PRIMARY KEY (`myColumn`, `secondColumn`));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" INTEGER, "secondColumn" TEXT, PRIMARY KEY ("myColumn", "secondColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" INTEGER, "secondColumn" TEXT,PRIMARY KEY ("myColumn", "secondColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -439,6 +463,7 @@ describe('QueryGenerator#createTableQuery', () => { sqlite3: 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` INTEGER NOT NULL, `secondColumn` INTEGER NOT NULL, `thirdColumn` TEXT NOT NULL, PRIMARY KEY (`secondColumn`, `thirdColumn`));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" INTEGER NOT NULL, "secondColumn" INTEGER NOT NULL, "thirdColumn" TEXT, PRIMARY KEY ("secondColumn", "thirdColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" INTEGER NOT NULL, "secondColumn" INTEGER NOT NULL, "thirdColumn" TEXT,PRIMARY KEY ("secondColumn", "thirdColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -455,6 +480,7 @@ describe('QueryGenerator#createTableQuery', () => { sqlite3: 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` INTEGER PRIMARY KEY AUTOINCREMENT);', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" INTEGER AUTOINCREMENT, PRIMARY KEY ("myColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" INTEGER AUTOINCREMENT,PRIMARY KEY ("myColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, snowflake: 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" INTEGER DEFAULT "myTable_myColumn_seq".NEXTVAL, PRIMARY KEY ("myColumn"));', }, @@ -472,6 +498,7 @@ describe('QueryGenerator#createTableQuery', () => { mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] INTEGER(5) UNSIGNED, PRIMARY KEY ([myColumn]));`, sqlite3: 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` INTEGER PRIMARY KEY);', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" INTEGER(5) UNSIGNED, PRIMARY KEY ("myColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" INTEGER(5) UNSIGNED,PRIMARY KEY ("myColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -483,6 +510,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS `myTable` (`myColumn` INTEGER(5) UNSIGNED) ENGINE=InnoDB;', mssql: `IF OBJECT_ID(N'[myTable]', 'U') IS NULL CREATE TABLE [myTable] ([myColumn] INTEGER(5) UNSIGNED);`, ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" INTEGER(5) UNSIGNED); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" INTEGER(5) UNSIGNED)'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); @@ -498,6 +526,7 @@ describe('QueryGenerator#createTableQuery', () => { 'snowflake db2': 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" INTEGER, FOREIGN KEY ("myColumn") REFERENCES "Bar" ("id"));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" INTEGER REFERENCES "Bar" ("id")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" INTEGER,FOREIGN KEY ("myColumn") REFERENCES "Bar" ("id"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -608,6 +637,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE, "secondColumn" TEXT, UNIQUE "uniq_myTable_myColumn_secondColumn" ("myColumn", "secondColumn"));', db2: 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE NOT NULL, "secondColumn" TEXT NOT NULL, CONSTRAINT "uniq_myTable_myColumn_secondColumn" UNIQUE ("myColumn", "secondColumn"));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE, "secondColumn" TEXT, CONSTRAINT "uniq_myTable_myColumn_secondColumn" UNIQUE ("myColumn", "secondColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE, "secondColumn" TEXT, CONSTRAINT "uniq_myTable_myColumn_secondColumn" UNIQUE ("myColumn", "secondColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -631,6 +661,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE, "secondColumn" TEXT, UNIQUE "myIndex" ("myColumn", "secondColumn"));', db2: 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE NOT NULL, "secondColumn" TEXT NOT NULL, CONSTRAINT "myIndex" UNIQUE ("myColumn", "secondColumn"));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE, "secondColumn" TEXT, CONSTRAINT "myIndex" UNIQUE ("myColumn", "secondColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE, "secondColumn" TEXT, CONSTRAINT "myIndex" UNIQUE ("myColumn", "secondColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -654,6 +685,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE, UNIQUE "uniq_myTable_myColumn" ("myColumn"));', db2: 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE NOT NULL, CONSTRAINT "uniq_myTable_myColumn" UNIQUE ("myColumn"));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE, CONSTRAINT "uniq_myTable_myColumn" UNIQUE ("myColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE, CONSTRAINT "uniq_myTable_myColumn" UNIQUE ("myColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -677,6 +709,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE, "secondColumn" TEXT, UNIQUE "uniq_myTable_myColumn_secondColumn" ("myColumn", "secondColumn"), PRIMARY KEY ("myColumn", "secondColumn"));', db2: 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE, "secondColumn" TEXT, CONSTRAINT "uniq_myTable_myColumn_secondColumn" UNIQUE ("myColumn", "secondColumn"), PRIMARY KEY ("myColumn", "secondColumn"));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE, "secondColumn" TEXT, PRIMARY KEY ("myColumn", "secondColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE, "secondColumn" TEXT,PRIMARY KEY ("myColumn", "secondColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -700,6 +733,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE NOT NULL, "secondColumn" TEXT, UNIQUE "uniq_myTable_myColumn_secondColumn" ("myColumn", "secondColumn"));', db2: 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE NOT NULL, "secondColumn" TEXT NOT NULL, CONSTRAINT "uniq_myTable_myColumn_secondColumn" UNIQUE ("myColumn", "secondColumn"));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE NOT NULL, "secondColumn" TEXT, CONSTRAINT "uniq_myTable_myColumn_secondColumn" UNIQUE ("myColumn", "secondColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE NOT NULL, "secondColumn" TEXT, CONSTRAINT "uniq_myTable_myColumn_secondColumn" UNIQUE ("myColumn", "secondColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); @@ -723,6 +757,7 @@ describe('QueryGenerator#createTableQuery', () => { 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE, "secondColumn" TEXT, UNIQUE "uniq_myTable_myColumn_secondColumn" ("myColumn", "secondColumn"), PRIMARY KEY ("myColumn"), FOREIGN KEY ("myColumn") REFERENCES "Bar" ("id"));', db2: 'CREATE TABLE IF NOT EXISTS "myTable" ("myColumn" DATE, "secondColumn" TEXT NOT NULL, CONSTRAINT "uniq_myTable_myColumn_secondColumn" UNIQUE ("myColumn", "secondColumn"), PRIMARY KEY ("myColumn"), FOREIGN KEY ("myColumn") REFERENCES "Bar" ("id"));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "myTable" ("myColumn" DATE REFERENCES "Bar" ("id"), "secondColumn" TEXT, CONSTRAINT "uniq_myTable_myColumn_secondColumn" UNIQUE ("myColumn", "secondColumn"), PRIMARY KEY ("myColumn")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "myTable" ("myColumn" DATE , "secondColumn" TEXT,PRIMARY KEY ("myColumn"),FOREIGN KEY ("myColumn") REFERENCES "Bar" ("id"), CONSTRAINT "uniq_myTable_myColumn_secondColumn" UNIQUE ("myColumn", "secondColumn"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }, ); }); diff --git a/packages/core/test/unit/query-generator/describe-table-query.test.ts b/packages/core/test/unit/query-generator/describe-table-query.test.ts index 84ab12a60bc2..6b068f7594bd 100644 --- a/packages/core/test/unit/query-generator/describe-table-query.test.ts +++ b/packages/core/test/unit/query-generator/describe-table-query.test.ts @@ -75,6 +75,10 @@ describe('QueryGenerator#describeTableQuery', () => { ON QSYS2.SYSCSTCOL.CONSTRAINT_NAME = QSYS2.SYSCST.CONSTRAINT_NAME WHERE QSYS2.SYSCOLUMNS.TABLE_SCHEMA = CURRENT SCHEMA AND QSYS2.SYSCOLUMNS.TABLE_NAME = 'myTable'`, + oracle: `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 (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) + WHERE (atc.OWNER = '${dialect.getDefaultSchema()}') AND (atc.TABLE_NAME = 'myTable')ORDER BY atc.COLUMN_NAME, CONSTRAINT_TYPE DESC`, }); }); @@ -150,6 +154,10 @@ describe('QueryGenerator#describeTableQuery', () => { ON QSYS2.SYSCSTCOL.CONSTRAINT_NAME = QSYS2.SYSCST.CONSTRAINT_NAME WHERE QSYS2.SYSCOLUMNS.TABLE_SCHEMA = CURRENT SCHEMA AND QSYS2.SYSCOLUMNS.TABLE_NAME = 'MyModels'`, + oracle: `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 (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) + WHERE (atc.OWNER = '${dialect.getDefaultSchema()}') AND (atc.TABLE_NAME = 'MyModels')ORDER BY atc.COLUMN_NAME, CONSTRAINT_TYPE DESC`, }); }); @@ -226,6 +234,11 @@ describe('QueryGenerator#describeTableQuery', () => { ON QSYS2.SYSCSTCOL.CONSTRAINT_NAME = QSYS2.SYSCST.CONSTRAINT_NAME WHERE QSYS2.SYSCOLUMNS.TABLE_SCHEMA = CURRENT SCHEMA AND QSYS2.SYSCOLUMNS.TABLE_NAME = 'MyModels'`, + oracle: `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 (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) WHERE (atc.OWNER = '${dialect.getDefaultSchema()}') AND (atc.TABLE_NAME = 'MyModels')ORDER BY atc.COLUMN_NAME, CONSTRAINT_TYPE DESC`, }); }); @@ -299,6 +312,10 @@ describe('QueryGenerator#describeTableQuery', () => { ON QSYS2.SYSCSTCOL.CONSTRAINT_NAME = QSYS2.SYSCST.CONSTRAINT_NAME WHERE QSYS2.SYSCOLUMNS.TABLE_SCHEMA = 'mySchema' AND QSYS2.SYSCOLUMNS.TABLE_NAME = 'myTable'`, + oracle: `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 (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) + WHERE (atc.OWNER = 'mySchema') AND (atc.TABLE_NAME = 'myTable')ORDER BY atc.COLUMN_NAME, CONSTRAINT_TYPE DESC`, }, ); }); @@ -378,6 +395,10 @@ describe('QueryGenerator#describeTableQuery', () => { ON QSYS2.SYSCSTCOL.CONSTRAINT_NAME = QSYS2.SYSCST.CONSTRAINT_NAME WHERE QSYS2.SYSCOLUMNS.TABLE_SCHEMA = CURRENT SCHEMA AND QSYS2.SYSCOLUMNS.TABLE_NAME = 'myTable'`, + oracle: `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 (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) + WHERE (atc.OWNER = '${dialect.getDefaultSchema()}') AND (atc.TABLE_NAME = 'myTable')ORDER BY atc.COLUMN_NAME, CONSTRAINT_TYPE DESC`, }, ); }); @@ -455,6 +476,10 @@ describe('QueryGenerator#describeTableQuery', () => { ON QSYS2.SYSCSTCOL.CONSTRAINT_NAME = QSYS2.SYSCST.CONSTRAINT_NAME WHERE QSYS2.SYSCOLUMNS.TABLE_SCHEMA = 'mySchema' AND QSYS2.SYSCOLUMNS.TABLE_NAME = 'myTable'`, + oracle: `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 (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) + WHERE (atc.OWNER = 'mySchema') AND (atc.TABLE_NAME = 'myTable')ORDER BY atc.COLUMN_NAME, CONSTRAINT_TYPE DESC`, }); }); diff --git a/packages/core/test/unit/query-generator/drop-schema-query.test.ts b/packages/core/test/unit/query-generator/drop-schema-query.test.ts index 12c5623df622..ec2bb90467b9 100644 --- a/packages/core/test/unit/query-generator/drop-schema-query.test.ts +++ b/packages/core/test/unit/query-generator/drop-schema-query.test.ts @@ -13,6 +13,7 @@ describe('QueryGenerator#dropSchemaQuery', () => { default: 'DROP SCHEMA [mySchema]', db2: 'DROP SCHEMA "mySchema" RESTRICT', sqlite3: notSupportedError, + oracle: `BEGIN EXECUTE IMMEDIATE 'DROP USER "mySchema" CASCADE' ; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -1918 THEN RAISE; END IF; END;`, }); }); @@ -21,6 +22,7 @@ describe('QueryGenerator#dropSchemaQuery', () => { default: 'DROP SCHEMA IF EXISTS [mySchema]', 'db2 mssql': buildInvalidOptionReceivedError('dropSchemaQuery', dialectName, ['ifExists']), sqlite3: notSupportedError, + oracle: `BEGIN EXECUTE IMMEDIATE 'DROP USER "mySchema" CASCADE' ; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -1918 THEN RAISE; END IF; END;`, }); }); @@ -31,6 +33,7 @@ describe('QueryGenerator#dropSchemaQuery', () => { 'cascade', ]), sqlite3: notSupportedError, + oracle: `BEGIN EXECUTE IMMEDIATE 'DROP USER "mySchema" CASCADE' ; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -1918 THEN RAISE; END IF; END;`, }); }); @@ -43,6 +46,7 @@ describe('QueryGenerator#dropSchemaQuery', () => { ]), 'mariadb mysql': buildInvalidOptionReceivedError('dropSchemaQuery', dialectName, ['cascade']), sqlite3: notSupportedError, + oracle: `BEGIN EXECUTE IMMEDIATE 'DROP USER "mySchema" CASCADE' ; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -1918 THEN RAISE; END IF; END;`, }); }); }); diff --git a/packages/core/test/unit/query-generator/drop-table-query.test.ts b/packages/core/test/unit/query-generator/drop-table-query.test.ts index de34dd6df782..b32fbb1df4b3 100644 --- a/packages/core/test/unit/query-generator/drop-table-query.test.ts +++ b/packages/core/test/unit/query-generator/drop-table-query.test.ts @@ -10,6 +10,7 @@ describe('QueryGenerator#dropTableQuery', () => { it('produces a query that drops a table', () => { expectsql(() => queryGenerator.dropTableQuery('myTable'), { default: `DROP TABLE IF EXISTS [myTable]`, + oracle: `BEGIN EXECUTE IMMEDIATE 'DROP TABLE "myTable" CASCADE CONSTRAINTS PURGE'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;`, }); }); @@ -17,6 +18,7 @@ describe('QueryGenerator#dropTableQuery', () => { expectsql(() => queryGenerator.dropTableQuery('myTable', { cascade: true }), { default: buildInvalidOptionReceivedError('dropTableQuery', dialectName, ['cascade']), 'postgres snowflake': `DROP TABLE IF EXISTS "myTable" CASCADE`, + oracle: `BEGIN EXECUTE IMMEDIATE 'DROP TABLE "myTable" CASCADE CONSTRAINTS PURGE'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;`, }); }); @@ -25,6 +27,7 @@ describe('QueryGenerator#dropTableQuery', () => { expectsql(() => queryGenerator.dropTableQuery(MyModel), { default: `DROP TABLE IF EXISTS [MyModels]`, + oracle: `BEGIN EXECUTE IMMEDIATE 'DROP TABLE "MyModels" CASCADE CONSTRAINTS PURGE'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;`, }); }); @@ -34,6 +37,7 @@ describe('QueryGenerator#dropTableQuery', () => { expectsql(() => queryGenerator.dropTableQuery(myDefinition), { default: `DROP TABLE IF EXISTS [MyModels]`, + oracle: `BEGIN EXECUTE IMMEDIATE 'DROP TABLE "MyModels" CASCADE CONSTRAINTS PURGE'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;`, }); }); @@ -41,6 +45,7 @@ describe('QueryGenerator#dropTableQuery', () => { expectsql(() => queryGenerator.dropTableQuery({ tableName: 'myTable', schema: 'mySchema' }), { default: `DROP TABLE IF EXISTS [mySchema].[myTable]`, sqlite3: 'DROP TABLE IF EXISTS `mySchema.myTable`', + oracle: `BEGIN EXECUTE IMMEDIATE 'DROP TABLE "mySchema"."myTable" CASCADE CONSTRAINTS PURGE'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;`, }); }); @@ -50,6 +55,7 @@ describe('QueryGenerator#dropTableQuery', () => { queryGenerator.dropTableQuery({ tableName: 'myTable', schema: dialect.getDefaultSchema() }), { default: `DROP TABLE IF EXISTS [myTable]`, + oracle: `BEGIN EXECUTE IMMEDIATE 'DROP TABLE "myTable" CASCADE CONSTRAINTS PURGE'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;`, }, ); }); @@ -61,6 +67,7 @@ describe('QueryGenerator#dropTableQuery', () => { expectsql(() => queryGeneratorSchema.dropTableQuery('myTable'), { default: `DROP TABLE IF EXISTS [mySchema].[myTable]`, sqlite3: 'DROP TABLE IF EXISTS `mySchema.myTable`', + oracle: `BEGIN EXECUTE IMMEDIATE 'DROP TABLE "mySchema"."myTable" CASCADE CONSTRAINTS PURGE'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;`, }); }); diff --git a/packages/core/test/unit/query-generator/get-constraint-snippet.test.ts b/packages/core/test/unit/query-generator/get-constraint-snippet.test.ts index 64af232085bb..1c780e17bb87 100644 --- a/packages/core/test/unit/query-generator/get-constraint-snippet.test.ts +++ b/packages/core/test/unit/query-generator/get-constraint-snippet.test.ts @@ -533,7 +533,7 @@ describe('QueryGeneratorInternal#getConstraintSnippet', () => { }), { default: `CONSTRAINT [myTable_otherId_otherTable_fk] FOREIGN KEY ([otherId]) REFERENCES [otherTable] ([id]) ON UPDATE CASCADE`, - 'db2 ibmi': onUpdateNotSupportedError, + 'db2 ibmi oracle': onUpdateNotSupportedError, }, ); }); diff --git a/packages/core/test/unit/query-generator/insert-query.test.ts b/packages/core/test/unit/query-generator/insert-query.test.ts index 35078ad7c33b..c0aad396f617 100644 --- a/packages/core/test/unit/query-generator/insert-query.test.ts +++ b/packages/core/test/unit/query-generator/insert-query.test.ts @@ -4,6 +4,7 @@ import { beforeAll2, expectsql, sequelize } from '../../support'; describe('QueryGenerator#insertQuery', () => { const queryGenerator = sequelize.queryGenerator; + const dialect = sequelize.dialect; const vars = beforeAll2(() => { const User = sequelize.define( @@ -56,6 +57,7 @@ describe('QueryGenerator#insertQuery', () => { default: `INSERT INTO [Users] ([firstName],[lastName],[username]) VALUES ($sequelize_1,$lastName,$sequelize_2);`, db2: `SELECT * FROM FINAL TABLE (INSERT INTO "Users" ("firstName","lastName","username") VALUES ($sequelize_1,$lastName,$sequelize_2));`, ibmi: `SELECT * FROM FINAL TABLE (INSERT INTO "Users" ("firstName","lastName","username") VALUES ($sequelize_1,$lastName,$sequelize_2))`, + oracle: `INSERT INTO "Users" ("firstName","lastName","username") VALUES (:1,$lastName,:2);`, }); expect(bind).to.deep.eq({ @@ -78,6 +80,7 @@ describe('QueryGenerator#insertQuery', () => { default: `INSERT INTO [Users] ([firstName],[lastName],[username]) VALUES ($sequelize_1,$1,$sequelize_2);`, db2: `SELECT * FROM FINAL TABLE (INSERT INTO "Users" ("firstName","lastName","username") VALUES ($sequelize_1,$1,$sequelize_2));`, ibmi: `SELECT * FROM FINAL TABLE (INSERT INTO "Users" ("firstName","lastName","username") VALUES ($sequelize_1,$1,$sequelize_2))`, + oracle: `INSERT INTO "Users" ("firstName","lastName","username") VALUES (:1,$1,:2);`, }); expect(bind).to.deep.eq({ sequelize_1: 'John', @@ -175,10 +178,16 @@ describe('QueryGenerator#insertQuery', () => { 'INSERT INTO [Users] ([firstName]) OUTPUT INSERTED.[id], INSERTED.[firstName] VALUES ($sequelize_1);', db2: 'SELECT * FROM FINAL TABLE (INSERT INTO "Users" ("firstName") VALUES ($sequelize_1));', ibmi: 'SELECT * FROM FINAL TABLE (INSERT INTO "Users" ("firstName") VALUES ($sequelize_1))', + oracle: `INSERT INTO "Users" ("firstName") VALUES (:1) RETURNING "id", "firstName" INTO :2,:3;`, }); }); it('supports array of strings (column names)', () => { + // node-oracledb requires OUTBIND definition, RETURNING '*' isn't valid for oracle. + if (dialect.name === 'oracle') { + return; + } + const { User } = vars; const { query } = queryGenerator.insertQuery( @@ -207,6 +216,11 @@ describe('QueryGenerator#insertQuery', () => { }); it('supports array of literals', () => { + // node-oracledb requires OUTBIND definition, '*' isn't valid for oracle. + if (dialect.name === 'oracle') { + return; + } + const { User } = vars; expectsql( @@ -247,6 +261,7 @@ describe('QueryGenerator#insertQuery', () => { default: 'INSERT INTO [myTable] ([birthday]) VALUES ($sequelize_1);', 'db2 ibmi': 'SELECT * FROM FINAL TABLE (INSERT INTO "myTable" ("birthday") VALUES ($sequelize_1));', + oracle: `INSERT INTO "myTable" ("birthday") VALUES (:1);`, }, bind: { mysql: { @@ -273,6 +288,9 @@ describe('QueryGenerator#insertQuery', () => { mssql: { sequelize_1: '2011-03-27 10:01:55.000 +00:00', }, + oracle: { + sequelize_1: new Date('2011-03-27T10:01:55Z'), + }, }, }); }); @@ -285,6 +303,7 @@ describe('QueryGenerator#insertQuery', () => { 'INSERT INTO [myTable] ([positive],[negative]) VALUES ($sequelize_1,$sequelize_2);', 'db2 ibmi': 'SELECT * FROM FINAL TABLE (INSERT INTO "myTable" ("positive","negative") VALUES ($sequelize_1,$sequelize_2));', + oracle: `INSERT INTO "myTable" ("positive","negative") VALUES (:1,:2);`, }, bind: { sqlite3: { @@ -319,6 +338,10 @@ describe('QueryGenerator#insertQuery', () => { sequelize_1: true, sequelize_2: false, }, + oracle: { + sequelize_1: '1', + sequelize_2: '0', + }, }, }); }); @@ -333,6 +356,7 @@ describe('QueryGenerator#insertQuery', () => { default: 'INSERT INTO [myTable] ([value],[name]) VALUES ($sequelize_1,$sequelize_2);', 'db2 ibmi': 'SELECT * FROM FINAL TABLE (INSERT INTO "myTable" ("value","name") VALUES ($sequelize_1,$sequelize_2));', + oracle: `INSERT INTO "myTable" ("value","name") VALUES (:1,:2);`, }); expect(bind).to.deep.eq({ diff --git a/packages/core/test/unit/query-generator/json-path-extraction-query.test.ts b/packages/core/test/unit/query-generator/json-path-extraction-query.test.ts index 7edfea4feed6..632df98eea13 100644 --- a/packages/core/test/unit/query-generator/json-path-extraction-query.test.ts +++ b/packages/core/test/unit/query-generator/json-path-extraction-query.test.ts @@ -24,6 +24,7 @@ describe('QueryGenerator#jsonPathExtractionQuery', () => { mariadb: `json_compact(json_extract(\`profile\`,'$.id'))`, 'mysql sqlite3': `json_extract(\`profile\`,'$.id')`, postgres: `"profile"->'id'`, + oracle: `json_value("profile",'$."id"')`, }, ); }); @@ -41,6 +42,7 @@ describe('QueryGenerator#jsonPathExtractionQuery', () => { mariadb: `json_compact(json_extract(\`profile\`,'$[0]'))`, 'mysql sqlite3': `json_extract(\`profile\`,'$[0]')`, postgres: `"profile"->0`, + oracle: `json_value("profile",'$[0]')`, }, ); }); @@ -58,6 +60,7 @@ describe('QueryGenerator#jsonPathExtractionQuery', () => { mariadb: `json_compact(json_extract(\`profile\`,'$.id.username[0]."0".name'))`, 'mysql sqlite3': `json_extract(\`profile\`,'$.id.username[0]."0".name')`, postgres: `"profile"#>ARRAY['id','username','0','0','name']::VARCHAR(255)[]`, + oracle: `json_value("profile",'$."id"."username"[0][0]."name"')`, }, ); }); @@ -76,6 +79,7 @@ describe('QueryGenerator#jsonPathExtractionQuery', () => { mariadb: `json_compact(json_extract(\`profile\`,'$."\\\\""."\\'"."$"'))`, sqlite3: `json_extract(\`profile\`,'$."\\""."''"."$"')`, postgres: `"profile"#>ARRAY['"','''','$']::VARCHAR(255)[]`, + oracle: `json_value("profile",'$.""."''"."$"')`, }, ); }); diff --git a/packages/core/test/unit/query-generator/list-schemas-query.test.ts b/packages/core/test/unit/query-generator/list-schemas-query.test.ts index c63ae40a7753..218006362fd4 100644 --- a/packages/core/test/unit/query-generator/list-schemas-query.test.ts +++ b/packages/core/test/unit/query-generator/list-schemas-query.test.ts @@ -17,6 +17,7 @@ describe('QueryGenerator#listSchemasQuery', () => { sqlite3: notSupportedError, postgres: `SELECT schema_name AS "schema" FROM information_schema.schemata WHERE schema_name !~ E'^pg_' AND schema_name NOT IN ('public', 'information_schema', 'tiger', 'tiger_data', 'topology')`, snowflake: `SELECT SCHEMA_NAME AS "schema" FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('INFORMATION_SCHEMA', 'PERFORMANCE_SCHEMA', 'SYS', 'information_schema', 'performance_schema', 'sys')`, + oracle: `SELECT USERNAME AS "schema" FROM ALL_USERS WHERE COMMON = ('NO') AND USERNAME != user`, }); }); @@ -30,6 +31,7 @@ describe('QueryGenerator#listSchemasQuery', () => { sqlite3: notSupportedError, postgres: `SELECT schema_name AS "schema" FROM information_schema.schemata WHERE schema_name !~ E'^pg_' AND schema_name NOT IN ('public', 'information_schema', 'tiger', 'tiger_data', 'topology', 'test', 'Te''st2')`, snowflake: `SELECT SCHEMA_NAME AS "schema" FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('INFORMATION_SCHEMA', 'PERFORMANCE_SCHEMA', 'SYS', 'information_schema', 'performance_schema', 'sys', 'test', 'Te''st2')`, + oracle: `SELECT USERNAME AS "schema" FROM ALL_USERS WHERE COMMON = ('NO') AND USERNAME != user`, }); }); }); diff --git a/packages/core/test/unit/query-generator/list-tables-query.test.ts b/packages/core/test/unit/query-generator/list-tables-query.test.ts index 536e5a7fba0a..ae1397e915a1 100644 --- a/packages/core/test/unit/query-generator/list-tables-query.test.ts +++ b/packages/core/test/unit/query-generator/list-tables-query.test.ts @@ -14,6 +14,7 @@ describe('QueryGenerator#listTablesQuery', () => { mariadb: `SELECT TABLE_NAME AS \`tableName\`, TABLE_SCHEMA AS \`schema\` FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA NOT IN ('MYSQL', 'INFORMATION_SCHEMA', 'PERFORMANCE_SCHEMA', 'SYS', 'mysql', 'information_schema', 'performance_schema', 'sys') ORDER BY TABLE_SCHEMA, TABLE_NAME`, postgres: `SELECT table_name AS "tableName", table_schema AS "schema" FROM information_schema.tables WHERE table_type = 'BASE TABLE' AND table_name != 'spatial_ref_sys' AND table_schema !~ E'^pg_' AND table_schema NOT IN ('information_schema', 'tiger', 'tiger_data', 'topology') ORDER BY table_schema, table_name`, snowflake: `SELECT TABLE_NAME AS "tableName", TABLE_SCHEMA AS "schema" FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA NOT IN ('INFORMATION_SCHEMA', 'PERFORMANCE_SCHEMA', 'SYS', 'information_schema', 'performance_schema', 'sys') ORDER BY TABLE_SCHEMA, TABLE_NAME`, + oracle: `SELECT owner as "schema", table_name as "tableName" FROM all_tables where OWNER IN(SELECT USERNAME AS "schema_name" FROM ALL_USERS WHERE ORACLE_MAINTAINED = 'N')`, }); }); @@ -27,6 +28,7 @@ describe('QueryGenerator#listTablesQuery', () => { mariadb: `SELECT TABLE_NAME AS \`tableName\`, TABLE_SCHEMA AS \`schema\` FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = 'mySchema' ORDER BY TABLE_SCHEMA, TABLE_NAME`, postgres: `SELECT table_name AS "tableName", table_schema AS "schema" FROM information_schema.tables WHERE table_type = 'BASE TABLE' AND table_name != 'spatial_ref_sys' AND table_schema = 'mySchema' ORDER BY table_schema, table_name`, snowflake: `SELECT TABLE_NAME AS "tableName", TABLE_SCHEMA AS "schema" FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = 'mySchema' ORDER BY TABLE_SCHEMA, TABLE_NAME`, + oracle: `SELECT owner as "schema", table_name as "tableName" FROM all_tables where OWNER IN(SELECT USERNAME AS "schema_name" FROM ALL_USERS WHERE ORACLE_MAINTAINED = 'N' AND USERNAME='mySchema')`, }); }); @@ -42,6 +44,7 @@ describe('QueryGenerator#listTablesQuery', () => { mariadb: `SELECT TABLE_NAME AS \`tableName\`, TABLE_SCHEMA AS \`schema\` FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = 'sequelize_test' ORDER BY TABLE_SCHEMA, TABLE_NAME`, postgres: `SELECT table_name AS "tableName", table_schema AS "schema" FROM information_schema.tables WHERE table_type = 'BASE TABLE' AND table_name != 'spatial_ref_sys' AND table_schema = 'public' ORDER BY table_schema, table_name`, snowflake: `SELECT TABLE_NAME AS "tableName", TABLE_SCHEMA AS "schema" FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = 'PUBLIC' ORDER BY TABLE_SCHEMA, TABLE_NAME`, + oracle: `SELECT owner as "schema", table_name as "tableName" FROM all_tables where OWNER IN(SELECT USERNAME AS "schema_name" FROM ALL_USERS WHERE ORACLE_MAINTAINED = 'N' AND USERNAME='${sequelize.dialect.getDefaultSchema()}')`, }, ); }); diff --git a/packages/core/test/unit/query-generator/remove-constraint-query.test.ts b/packages/core/test/unit/query-generator/remove-constraint-query.test.ts index fc556e7563f4..6d7974f82b72 100644 --- a/packages/core/test/unit/query-generator/remove-constraint-query.test.ts +++ b/packages/core/test/unit/query-generator/remove-constraint-query.test.ts @@ -20,13 +20,11 @@ describe('QueryGenerator#removeConstraintQuery', () => { expectsql( () => queryGenerator.removeConstraintQuery('myTable', 'myConstraint', { ifExists: true }), { - default: 'ALTER TABLE [myTable] DROP CONSTRAINT IF EXISTS [myConstraint]', + default: buildInvalidOptionReceivedError('removeConstraintQuery', dialect.name, [ + 'ifExists', + ]), + 'postgres mariadb mssql': 'ALTER TABLE [myTable] DROP CONSTRAINT IF EXISTS [myConstraint]', sqlite3: notSupportedError, - 'db2 ibmi mysql snowflake': buildInvalidOptionReceivedError( - 'removeConstraintQuery', - dialect.name, - ['ifExists'], - ), }, ); }); diff --git a/packages/core/test/unit/query-generator/remove-index-query.test.ts b/packages/core/test/unit/query-generator/remove-index-query.test.ts index 8e28f0374c72..d28e5189b7e4 100644 --- a/packages/core/test/unit/query-generator/remove-index-query.test.ts +++ b/packages/core/test/unit/query-generator/remove-index-query.test.ts @@ -15,7 +15,7 @@ describe('QueryGenerator#removeIndexQuery', () => { default: `DROP INDEX [user_foo_bar] ON [myTable]`, sqlite3: 'DROP INDEX `user_foo_bar`', ibmi: `BEGIN DROP INDEX "user_foo_bar"; COMMIT; END`, - db2: `DROP INDEX "user_foo_bar"`, + 'db2 oracle': `DROP INDEX "user_foo_bar"`, postgres: `DROP INDEX "public"."user_foo_bar"`, snowflake: notImplementedError, }); @@ -26,7 +26,7 @@ describe('QueryGenerator#removeIndexQuery', () => { default: `DROP INDEX [my_table_foo_bar] ON [myTable]`, sqlite3: 'DROP INDEX `my_table_foo_bar`', ibmi: `BEGIN DROP INDEX "my_table_foo_bar"; COMMIT; END`, - db2: `DROP INDEX "my_table_foo_bar"`, + 'db2 oracle': `DROP INDEX "my_table_foo_bar"`, postgres: `DROP INDEX "public"."my_table_foo_bar"`, snowflake: notImplementedError, }); @@ -54,7 +54,7 @@ describe('QueryGenerator#removeIndexQuery', () => { postgres: `DROP INDEX IF EXISTS "public"."user_foo_bar"`, ibmi: `BEGIN IF EXISTS (SELECT * FROM QSYS2.SYSINDEXES WHERE INDEX_NAME = "user_foo_bar") THEN DROP INDEX "user_foo_bar"; COMMIT; END IF; END`, snowflake: notImplementedError, - 'db2 mysql': buildInvalidOptionReceivedError('removeIndexQuery', dialect.name, [ + 'db2 mysql oracle': buildInvalidOptionReceivedError('removeIndexQuery', dialect.name, [ 'ifExists', ]), }, @@ -80,7 +80,7 @@ describe('QueryGenerator#removeIndexQuery', () => { default: `DROP INDEX IF EXISTS [user_foo_bar] ON [myTable] CASCADE`, postgres: `DROP INDEX IF EXISTS "public"."user_foo_bar" CASCADE`, snowflake: notImplementedError, - 'db2 mysql': buildInvalidOptionReceivedError('removeIndexQuery', dialect.name, [ + 'db2 mysql oracle': buildInvalidOptionReceivedError('removeIndexQuery', dialect.name, [ 'cascade', 'ifExists', ]), @@ -104,7 +104,7 @@ describe('QueryGenerator#removeIndexQuery', () => { default: `DROP INDEX CONCURRENTLY IF EXISTS [user_foo_bar] ON [myTable]`, postgres: `DROP INDEX CONCURRENTLY IF EXISTS "public"."user_foo_bar"`, snowflake: notImplementedError, - 'db2 mysql': buildInvalidOptionReceivedError('removeIndexQuery', dialect.name, [ + 'db2 mysql oracle': buildInvalidOptionReceivedError('removeIndexQuery', dialect.name, [ 'concurrently', 'ifExists', ]), @@ -144,7 +144,7 @@ describe('QueryGenerator#removeIndexQuery', () => { default: `DROP INDEX [user_foo_bar] ON [MyModels]`, sqlite3: 'DROP INDEX `user_foo_bar`', ibmi: `BEGIN DROP INDEX "user_foo_bar"; COMMIT; END`, - db2: `DROP INDEX "user_foo_bar"`, + 'db2 oracle': `DROP INDEX "user_foo_bar"`, postgres: `DROP INDEX "public"."user_foo_bar"`, snowflake: notImplementedError, }); @@ -158,7 +158,7 @@ describe('QueryGenerator#removeIndexQuery', () => { default: `DROP INDEX [user_foo_bar] ON [MyModels]`, sqlite3: 'DROP INDEX `user_foo_bar`', ibmi: `BEGIN DROP INDEX "user_foo_bar"; COMMIT; END`, - db2: `DROP INDEX "user_foo_bar"`, + 'db2 oracle': `DROP INDEX "user_foo_bar"`, postgres: `DROP INDEX "public"."user_foo_bar"`, snowflake: notImplementedError, }); @@ -176,7 +176,7 @@ describe('QueryGenerator#removeIndexQuery', () => { sqlite3: 'DROP INDEX `user_foo_bar`', postgres: `DROP INDEX "mySchema"."user_foo_bar"`, ibmi: `BEGIN DROP INDEX "user_foo_bar"; COMMIT; END`, - db2: `DROP INDEX "user_foo_bar"`, + 'db2 oracle': `DROP INDEX "user_foo_bar"`, snowflake: notImplementedError, }, ); @@ -193,7 +193,7 @@ describe('QueryGenerator#removeIndexQuery', () => { default: `DROP INDEX [user_foo_bar] ON [myTable]`, sqlite3: 'DROP INDEX `user_foo_bar`', ibmi: `BEGIN DROP INDEX "user_foo_bar"; COMMIT; END`, - db2: `DROP INDEX "user_foo_bar"`, + 'db2 oracle': `DROP INDEX "user_foo_bar"`, postgres: `DROP INDEX "public"."user_foo_bar"`, snowflake: notImplementedError, }, @@ -209,7 +209,7 @@ describe('QueryGenerator#removeIndexQuery', () => { sqlite3: 'DROP INDEX `user_foo_bar`', postgres: `DROP INDEX "mySchema"."user_foo_bar"`, ibmi: `BEGIN DROP INDEX "user_foo_bar"; COMMIT; END`, - db2: 'DROP INDEX "user_foo_bar"', + 'db2 oracle': 'DROP INDEX "user_foo_bar"', snowflake: notImplementedError, }); }); diff --git a/packages/core/test/unit/query-generator/rename-table-query.test.ts b/packages/core/test/unit/query-generator/rename-table-query.test.ts index 312652f14d16..32e0ce9cd669 100644 --- a/packages/core/test/unit/query-generator/rename-table-query.test.ts +++ b/packages/core/test/unit/query-generator/rename-table-query.test.ts @@ -56,7 +56,7 @@ describe('QueryGenerator#renameTableQuery', () => { ), { default: changeSchemaNotSetError, - 'db2 ibmi': moveSchemaNotSupportedError, + 'db2 ibmi oracle': moveSchemaNotSupportedError, }, ); }); @@ -74,7 +74,7 @@ describe('QueryGenerator#renameTableQuery', () => { mssql: `ALTER SCHEMA [newSchema] TRANSFER [oldSchema].[oldTable]`, sqlite3: 'ALTER TABLE `oldSchema.oldTable` RENAME TO `newSchema.oldTable`', postgres: `ALTER TABLE "oldSchema"."oldTable" SET SCHEMA "newSchema"`, - 'db2 ibmi': buildInvalidOptionReceivedError('renameTableQuery', dialect.name, [ + 'db2 ibmi oracle': buildInvalidOptionReceivedError('renameTableQuery', dialect.name, [ 'changeSchema', ]), }, @@ -92,7 +92,7 @@ describe('QueryGenerator#renameTableQuery', () => { { default: 'ALTER TABLE [oldSchema].[oldTable] RENAME TO [newSchema].[newTable]', sqlite3: 'ALTER TABLE `oldSchema.oldTable` RENAME TO `newSchema.newTable`', - 'db2 ibmi': buildInvalidOptionReceivedError('renameTableQuery', dialect.name, [ + 'db2 ibmi oracle': buildInvalidOptionReceivedError('renameTableQuery', dialect.name, [ 'changeSchema', ]), 'mssql postgres': moveSchemaWithRenameNotSupportedError, diff --git a/packages/core/test/unit/query-generator/rollback-transaction-query.test.ts b/packages/core/test/unit/query-generator/rollback-transaction-query.test.ts index a69f3132d9a2..b0e9b894d819 100644 --- a/packages/core/test/unit/query-generator/rollback-transaction-query.test.ts +++ b/packages/core/test/unit/query-generator/rollback-transaction-query.test.ts @@ -12,6 +12,7 @@ describe('QueryGenerator#rollbackTransactionQuery', () => { expectsql(() => queryGenerator.rollbackTransactionQuery(), { default: 'ROLLBACK', 'db2 ibmi mssql': notSupportedError, + oracle: 'ROLLBACK TRANSACTION', }); }); }); diff --git a/packages/core/test/unit/query-generator/select-query.test.ts b/packages/core/test/unit/query-generator/select-query.test.ts index 902a03fa25c3..c50edd8cfd57 100644 --- a/packages/core/test/unit/query-generator/select-query.test.ts +++ b/packages/core/test/unit/query-generator/select-query.test.ts @@ -90,6 +90,7 @@ describe('QueryGenerator#selectQuery', () => { 'mariadb mysql': 'SELECT `id` FROM `Users` AS `User` ORDER BY `User`.`id` LIMIT 18446744073709551615 OFFSET 1;', 'db2 ibmi mssql': `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] OFFSET 1 ROWS;`, + oracle: `SELECT "id" FROM "Users" "User" ORDER BY "User"."id" OFFSET 1 ROWS;`, }); }); @@ -110,6 +111,7 @@ describe('QueryGenerator#selectQuery', () => { default: 'SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] LIMIT 10;', mssql: `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY;`, 'db2 ibmi': `SELECT "id" FROM "Users" AS "User" ORDER BY "User"."id" FETCH NEXT 10 ROWS ONLY;`, + oracle: `SELECT "id" FROM "Users" "User" ORDER BY "User"."id" OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY;`, }); }); @@ -130,6 +132,7 @@ describe('QueryGenerator#selectQuery', () => { expectsql(sql, { default: 'SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] LIMIT 10 OFFSET 1;', 'db2 ibmi mssql': `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] OFFSET 1 ROWS FETCH NEXT 10 ROWS ONLY;`, + oracle: `SELECT "id" FROM "Users" "User" ORDER BY "User"."id" OFFSET 1 ROWS FETCH NEXT 10 ROWS ONLY;`, }); }); @@ -151,6 +154,7 @@ describe('QueryGenerator#selectQuery', () => { default: `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] LIMIT 10;`, mssql: `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY;`, 'db2 ibmi': `SELECT "id" FROM "Users" AS "User" ORDER BY "User"."id" FETCH NEXT 10 ROWS ONLY;`, + oracle: `SELECT "id" FROM "Users" "User" ORDER BY "User"."id" OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY;`, }); }); @@ -169,6 +173,7 @@ describe('QueryGenerator#selectQuery', () => { expectsql(sql, { default: `SELECT [id] FROM [Users] AS [User];`, + oracle: `SELECT "id" FROM "Users" "User";`, }); }); @@ -190,6 +195,7 @@ describe('QueryGenerator#selectQuery', () => { default: `SELECT [id] FROM [Users] AS [User] ORDER BY [User].[id] LIMIT 0;`, mssql: new Error(`LIMIT 0 is not supported by ${dialectName} dialect.`), 'db2 ibmi': `SELECT "id" FROM "Users" AS "User" ORDER BY "User"."id" FETCH NEXT 0 ROWS ONLY;`, + oracle: `SELECT "id" FROM "Users" "User" ORDER BY "User"."id";`, }, ); }); @@ -214,6 +220,7 @@ describe('QueryGenerator#selectQuery', () => { 'db2 ibmi': `SELECT "id" FROM "Users" AS "User" ORDER BY "User"."id" FETCH NEXT ''';DELETE FROM user' ROWS ONLY;`, 'mariadb mysql': "SELECT `id` FROM `Users` AS `User` ORDER BY `User`.`id` LIMIT '\\';DELETE FROM user';", + oracle: `SELECT "id" FROM "Users" "User" ORDER BY "User"."id" OFFSET 0 ROWS FETCH NEXT ''';DELETE FROM user' ROWS ONLY;`, }); }); @@ -238,6 +245,7 @@ describe('QueryGenerator#selectQuery', () => { 'db2 ibmi': `SELECT "id" FROM "Users" AS "User" ORDER BY "User"."id" OFFSET ''';DELETE FROM user' ROWS FETCH NEXT 10 ROWS ONLY;`, 'mariadb mysql': "SELECT `id` FROM `Users` AS `User` ORDER BY `User`.`id` LIMIT 10 OFFSET '\\';DELETE FROM user';", + oracle: `SELECT "id" FROM "Users" "User" ORDER BY "User"."id" OFFSET ''';DELETE FROM user' ROWS FETCH NEXT 10 ROWS ONLY;`, }); }); }); @@ -259,6 +267,7 @@ describe('QueryGenerator#selectQuery', () => { expectsql(sql, { default: `SELECT [id] FROM [Projects] AS [Project] WHERE [Project].[duration] = 9007199254740993;`, + oracle: `SELECT "id" FROM "Projects" "Project" WHERE "Project"."duration" = 9007199254740993;`, }); }); @@ -276,6 +285,7 @@ describe('QueryGenerator#selectQuery', () => { expectsql(sql, { default: `SELECT [id], CAST([createdAt] AS VARCHAR) AS [createdAt] FROM [Users] AS [User];`, + oracle: `SELECT "id", CAST("createdAt" AS VARCHAR) AS "createdAt" FROM "Users" "User";`, }); }); @@ -294,6 +304,7 @@ describe('QueryGenerator#selectQuery', () => { expectsql(sql, { default: `SELECT [id] FROM [Users] AS [User];`, + oracle: `SELECT "id" FROM "Users" "User";`, }); }); @@ -314,6 +325,7 @@ describe('QueryGenerator#selectQuery', () => { default: `SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = 'foo'';DROP TABLE mySchema.myTable;';`, 'mysql mariadb': `SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = 'foo\\';DROP TABLE mySchema.myTable;';`, mssql: `SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = N'foo'';DROP TABLE mySchema.myTable;';`, + oracle: `SELECT "id" FROM "Users" "User" WHERE "User"."username" = 'foo'';DROP TABLE mySchema.myTable;';`, }); }); @@ -337,6 +349,7 @@ describe('QueryGenerator#selectQuery', () => { postgres: `SELECT "data"->'email' AS "email" FROM "Users" AS "User";`, mariadb: `SELECT json_compact(json_extract(\`data\`,'$.email')) AS \`email\` FROM \`Users\` AS \`User\`;`, 'sqlite3 mysql': `SELECT json_extract([data],'$.email') AS [email] FROM [Users] AS [User];`, + oracle: `SELECT json_value("data",'$."email"') AS "email" FROM "Users" "User";`, }); }); } @@ -411,6 +424,16 @@ describe('QueryGenerator#selectQuery', () => { OFFSET 'repl4' ROWS FETCH NEXT 'repl3' ROWS ONLY; `, + oracle: ` + SELECT uppercase('id') AS "id", 'id2' + FROM "Users" "User" + WHERE "User"."username" = 'repl1' OR "User"."username" = (uppercase(CAST('repl1' AS STRING)) = 'repl1') + GROUP BY 'the group' + HAVING "User"."username" = 'repl1' + ORDER BY 'repl2' + OFFSET 'repl4' ROWS + FETCH NEXT 'repl3' ROWS ONLY; + `, }); }); @@ -435,6 +458,7 @@ describe('QueryGenerator#selectQuery', () => { expectsql(sql, { default: `SELECT id FROM [Users] AS [User] WHERE id = ':id';`, + oracle: `SELECT id FROM "Users" "User" WHERE id = ':id';`, }); }); @@ -516,6 +540,19 @@ describe('QueryGenerator#selectQuery', () => { LEFT OUTER JOIN "Users" AS "projects->owner" ON "projects"."ownerId" = "projects->owner"."id" `, + oracle: ` + SELECT + "User"."id", + "projects"."id" AS "projects.id", + 'repl1', 'repl1' AS "projects.id2", + "projects->owner"."id" AS "projects.owner.id", + 'repl2' + FROM "Users" "User" + INNER JOIN "Projects" "projects" + ON 'on' AND 'where' + LEFT OUTER JOIN "Users" "projects->owner" + ON "projects"."ownerId" = "projects->owner"."id"; + `, }); }); @@ -577,6 +614,21 @@ describe('QueryGenerator#selectQuery', () => { ) ON [Project].[id] = [contributors->ProjectContributor].[projectId]; `, + oracle: ` + SELECT + "Project"."id", + "contributors"."id" AS "contributors.id", + "contributors->ProjectContributor"."userId" AS "contributors.ProjectContributor.userId", + "contributors->ProjectContributor"."projectId" AS "contributors.ProjectContributor.projectId" + FROM "Projects" "Project" + LEFT OUTER JOIN ( + "ProjectContributors" "contributors->ProjectContributor" + INNER JOIN "Users" "contributors" + ON "contributors"."id" = "contributors->ProjectContributor"."userId" + AND 'where' + ) + ON "Project"."id" = "contributors->ProjectContributor"."projectId"; + `, }); }); @@ -705,6 +757,26 @@ describe('QueryGenerator#selectQuery', () => { ON "projects"."ownerId" = "projects->owner"."id" ORDER BY 'order' `, + oracle: ` + SELECT + "User".*, + "projects"."id" AS "projects.id", + 'repl1', + 'repl1' AS "projects.id2", + "projects->owner"."id" AS "projects.owner.id", + 'repl2' FROM ( + SELECT "User"."id" + FROM "Users" "User" + ORDER BY 'order' + OFFSET 'offset' ROWS + FETCH NEXT 'limit' ROWS ONLY + ) "User" + INNER JOIN "Projects" "projects" + ON 'on' AND 'where' + LEFT OUTER JOIN "Users" "projects->owner" + ON "projects"."ownerId" = "projects->owner"."id" + ORDER BY 'order'; + `, }); }); @@ -783,6 +855,20 @@ Only named replacements (:name) are allowed in literal() because we cannot guara [a].* AS [col_a_all], * AS [col_all] FROM [Users] AS [User];`, + oracle: ` + SELECT + "count(*)" AS "count", + ".*", + "*", + count(*) AS "literal_count", + count('*') AS "fn_count_str", + count(*) AS "fn_count_col", + count(*) AS "fn_count_lit", + "a"."b" AS "col_a_b", + "a".* AS "col_a_all", + * AS "col_all" + FROM "Users" "User"; + `, }); }); @@ -802,6 +888,7 @@ Only named replacements (:name) are allowed in literal() because we cannot guara expectsql(sql, { default: `SELECT *, YEAR([createdAt]) AS [creationYear] FROM [Users] AS [User] GROUP BY [creationYear], [title] HAVING [User].[creationYear] > 2002;`, + oracle: `SELECT *, YEAR("createdAt") AS "creationYear" FROM "Users" "User" GROUP BY "creationYear", "title" HAVING "User"."creationYear" > 2002;`, }); }); }); @@ -937,6 +1024,7 @@ Only named replacements (:name) are allowed in literal() because we cannot guara expectsql(sql, { default: `SELECT 1 AS [_0] FROM [Users] AS [User] GROUP BY [_0] ORDER BY [_0];`, + oracle: `SELECT 1 AS "_0" FROM "Users" "User" GROUP BY "_0" ORDER BY "_0";`, }); }); }); diff --git a/packages/core/test/unit/query-generator/set-isolation-level-query.test.ts b/packages/core/test/unit/query-generator/set-isolation-level-query.test.ts index e411b822003f..5e9bf6703c6a 100644 --- a/packages/core/test/unit/query-generator/set-isolation-level-query.test.ts +++ b/packages/core/test/unit/query-generator/set-isolation-level-query.test.ts @@ -27,6 +27,7 @@ describe('QueryGenerator#setIsolationLevelQuery', () => { sqlite3: 'PRAGMA read_uncommitted = 1', snowflake: notSupportedError, 'db2 ibmi mssql': queryNotSupportedError, + oracle: 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED', }); }); @@ -38,6 +39,7 @@ describe('QueryGenerator#setIsolationLevelQuery', () => { ), snowflake: notSupportedError, 'db2 ibmi mssql': queryNotSupportedError, + oracle: 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE', }); }); diff --git a/packages/core/test/unit/query-generator/show-constraints-query.test.ts b/packages/core/test/unit/query-generator/show-constraints-query.test.ts index 13d268a0555a..4b4953964bea 100644 --- a/packages/core/test/unit/query-generator/show-constraints-query.test.ts +++ b/packages/core/test/unit/query-generator/show-constraints-query.test.ts @@ -16,6 +16,7 @@ describe('QueryGenerator#showConstraintsQuery', () => { snowflake: `SELECT c.CONSTRAINT_CATALOG AS "constraintCatalog", c.CONSTRAINT_SCHEMA AS "constraintSchema", c.CONSTRAINT_NAME AS "constraintName", c.CONSTRAINT_TYPE AS "constraintType", c.TABLE_CATALOG AS "tableCatalog", c.TABLE_SCHEMA AS "tableSchema", c.TABLE_NAME AS "tableName", fk.TABLE_SCHEMA AS "referencedTableSchema", fk.TABLE_NAME AS "referencedTableName", r.DELETE_RULE AS "deleteAction", r.UPDATE_RULE AS "updateAction", c.IS_DEFERRABLE AS "isDeferrable", c.INITIALLY_DEFERRED AS "initiallyDeferred" FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk ON r.UNIQUE_CONSTRAINT_CATALOG = fk.CONSTRAINT_CATALOG AND r.UNIQUE_CONSTRAINT_SCHEMA = fk.CONSTRAINT_SCHEMA AND r.UNIQUE_CONSTRAINT_NAME = fk.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'PUBLIC' ORDER BY c.CONSTRAINT_NAME`, sqlite3: `SELECT sql FROM sqlite_master WHERE tbl_name = 'myTable'`, 'mariadb mysql': `SELECT c.CONSTRAINT_SCHEMA AS constraintSchema, c.CONSTRAINT_NAME AS constraintName, c.CONSTRAINT_TYPE AS constraintType, c.TABLE_SCHEMA AS tableSchema, c.TABLE_NAME AS tableName, kcu.COLUMN_NAME AS columnNames, kcu.REFERENCED_TABLE_SCHEMA AS referencedTableSchema, kcu.REFERENCED_TABLE_NAME AS referencedTableName, kcu.REFERENCED_COLUMN_NAME AS referencedColumnNames, r.DELETE_RULE AS deleteAction, r.UPDATE_RULE AS updateAction, ch.CHECK_CLAUSE AS definition FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME AND c.TABLE_NAME = r.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON c.CONSTRAINT_CATALOG = kcu.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND c.TABLE_NAME = kcu.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS ch ON c.CONSTRAINT_CATALOG = ch.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = ch.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = ch.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'sequelize_test' ORDER BY c.CONSTRAINT_NAME, kcu.ORDINAL_POSITION`, + oracle: `SELECT C.CONSTRAINT_NAME "constraintName", CASE A.CONSTRAINT_TYPE WHEN 'P' THEN 'PRIMARY KEY' WHEN 'R' THEN 'FOREIGN KEY' WHEN 'C' THEN 'CHECK' WHEN 'U' THEN 'UNIQUE' ELSE NULL END "constraintType", C.TABLE_NAME "tableName", C.OWNER "constraintSchema", C.COLUMN_NAME "columnNames" FROM ALL_CONS_COLUMNS C INNER JOIN ALL_CONSTRAINTS A ON C.CONSTRAINT_NAME = A.CONSTRAINT_NAME WHERE C.TABLE_NAME ='myTable' AND C.OWNER ='${dialect.getDefaultSchema()}' ORDER BY C.CONSTRAINT_NAME`, }); }); @@ -27,6 +28,7 @@ describe('QueryGenerator#showConstraintsQuery', () => { postgres: `SELECT c.constraint_catalog AS "constraintCatalog", c.constraint_schema AS "constraintSchema", c.constraint_name AS "constraintName", c.constraint_type AS "constraintType", c.table_catalog AS "tableCatalog", c.table_schema AS "tableSchema", c.table_name AS "tableName", kcu.column_name AS "columnNames", ccu.table_schema AS "referencedTableSchema", ccu.table_name AS "referencedTableName", ccu.column_name AS "referencedColumnNames", r.delete_rule AS "deleteAction", r.update_rule AS "updateAction", pg_get_expr(pgc.conbin, pgc.conrelid) AS "definition", c.is_deferrable AS "isDeferrable", c.initially_deferred AS "initiallyDeferred" FROM INFORMATION_SCHEMA.table_constraints c LEFT JOIN INFORMATION_SCHEMA.referential_constraints r ON c.constraint_catalog = r.constraint_catalog AND c.constraint_schema = r.constraint_schema AND c.constraint_name = r.constraint_name LEFT JOIN INFORMATION_SCHEMA.key_column_usage kcu ON c.constraint_catalog = kcu.constraint_catalog AND c.constraint_schema = kcu.constraint_schema AND c.constraint_name = kcu.constraint_name LEFT JOIN information_schema.constraint_column_usage AS ccu ON r.constraint_catalog = ccu.constraint_catalog AND r.constraint_schema = ccu.constraint_schema AND r.constraint_name = ccu.constraint_name LEFT JOIN pg_constraint pgc ON c.constraint_name = pgc.conname AND c.table_schema = (SELECT nspname FROM pg_namespace WHERE oid = pgc.connamespace) AND c.table_name = pgc.conrelid::regclass::text WHERE c.table_name = 'myTable' AND c.table_schema = 'public' AND c.constraint_name = 'foo_bar' ORDER BY c.constraint_name, kcu.ordinal_position, ccu.column_name`, snowflake: `SELECT c.CONSTRAINT_CATALOG AS "constraintCatalog", c.CONSTRAINT_SCHEMA AS "constraintSchema", c.CONSTRAINT_NAME AS "constraintName", c.CONSTRAINT_TYPE AS "constraintType", c.TABLE_CATALOG AS "tableCatalog", c.TABLE_SCHEMA AS "tableSchema", c.TABLE_NAME AS "tableName", fk.TABLE_SCHEMA AS "referencedTableSchema", fk.TABLE_NAME AS "referencedTableName", r.DELETE_RULE AS "deleteAction", r.UPDATE_RULE AS "updateAction", c.IS_DEFERRABLE AS "isDeferrable", c.INITIALLY_DEFERRED AS "initiallyDeferred" FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk ON r.UNIQUE_CONSTRAINT_CATALOG = fk.CONSTRAINT_CATALOG AND r.UNIQUE_CONSTRAINT_SCHEMA = fk.CONSTRAINT_SCHEMA AND r.UNIQUE_CONSTRAINT_NAME = fk.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'PUBLIC' AND c.CONSTRAINT_NAME = 'foo_bar' ORDER BY c.CONSTRAINT_NAME`, sqlite3: `SELECT sql FROM sqlite_master WHERE tbl_name = 'myTable'`, + oracle: `SELECT C.CONSTRAINT_NAME "constraintName", CASE A.CONSTRAINT_TYPE WHEN 'P' THEN 'PRIMARY KEY' WHEN 'R' THEN 'FOREIGN KEY' WHEN 'C' THEN 'CHECK' WHEN 'U' THEN 'UNIQUE' ELSE NULL END "constraintType", C.TABLE_NAME "tableName", C.OWNER "constraintSchema", C.COLUMN_NAME "columnNames" FROM ALL_CONS_COLUMNS C INNER JOIN ALL_CONSTRAINTS A ON C.CONSTRAINT_NAME = A.CONSTRAINT_NAME WHERE C.TABLE_NAME ='myTable' AND C.OWNER ='${dialect.getDefaultSchema()}' AND C.CONSTRAINT_NAME ='foo_bar' ORDER BY C.CONSTRAINT_NAME`, 'mariadb mysql': `SELECT c.CONSTRAINT_SCHEMA AS constraintSchema, c.CONSTRAINT_NAME AS constraintName, c.CONSTRAINT_TYPE AS constraintType, c.TABLE_SCHEMA AS tableSchema, c.TABLE_NAME AS tableName, kcu.COLUMN_NAME AS columnNames, kcu.REFERENCED_TABLE_SCHEMA AS referencedTableSchema, kcu.REFERENCED_TABLE_NAME AS referencedTableName, kcu.REFERENCED_COLUMN_NAME AS referencedColumnNames, r.DELETE_RULE AS deleteAction, r.UPDATE_RULE AS updateAction, ch.CHECK_CLAUSE AS definition FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME AND c.TABLE_NAME = r.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON c.CONSTRAINT_CATALOG = kcu.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND c.TABLE_NAME = kcu.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS ch ON c.CONSTRAINT_CATALOG = ch.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = ch.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = ch.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'sequelize_test' AND c.CONSTRAINT_NAME = 'foo_bar' ORDER BY c.CONSTRAINT_NAME, kcu.ORDINAL_POSITION`, }); }); @@ -41,6 +43,7 @@ describe('QueryGenerator#showConstraintsQuery', () => { postgres: `SELECT c.constraint_catalog AS "constraintCatalog", c.constraint_schema AS "constraintSchema", c.constraint_name AS "constraintName", c.constraint_type AS "constraintType", c.table_catalog AS "tableCatalog", c.table_schema AS "tableSchema", c.table_name AS "tableName", kcu.column_name AS "columnNames", ccu.table_schema AS "referencedTableSchema", ccu.table_name AS "referencedTableName", ccu.column_name AS "referencedColumnNames", r.delete_rule AS "deleteAction", r.update_rule AS "updateAction", pg_get_expr(pgc.conbin, pgc.conrelid) AS "definition", c.is_deferrable AS "isDeferrable", c.initially_deferred AS "initiallyDeferred" FROM INFORMATION_SCHEMA.table_constraints c LEFT JOIN INFORMATION_SCHEMA.referential_constraints r ON c.constraint_catalog = r.constraint_catalog AND c.constraint_schema = r.constraint_schema AND c.constraint_name = r.constraint_name LEFT JOIN INFORMATION_SCHEMA.key_column_usage kcu ON c.constraint_catalog = kcu.constraint_catalog AND c.constraint_schema = kcu.constraint_schema AND c.constraint_name = kcu.constraint_name LEFT JOIN information_schema.constraint_column_usage AS ccu ON r.constraint_catalog = ccu.constraint_catalog AND r.constraint_schema = ccu.constraint_schema AND r.constraint_name = ccu.constraint_name LEFT JOIN pg_constraint pgc ON c.constraint_name = pgc.conname AND c.table_schema = (SELECT nspname FROM pg_namespace WHERE oid = pgc.connamespace) AND c.table_name = pgc.conrelid::regclass::text WHERE c.table_name = 'myTable' AND c.table_schema = 'public' AND c.constraint_type = 'FOREIGN KEY' ORDER BY c.constraint_name, kcu.ordinal_position, ccu.column_name`, snowflake: `SELECT c.CONSTRAINT_CATALOG AS "constraintCatalog", c.CONSTRAINT_SCHEMA AS "constraintSchema", c.CONSTRAINT_NAME AS "constraintName", c.CONSTRAINT_TYPE AS "constraintType", c.TABLE_CATALOG AS "tableCatalog", c.TABLE_SCHEMA AS "tableSchema", c.TABLE_NAME AS "tableName", fk.TABLE_SCHEMA AS "referencedTableSchema", fk.TABLE_NAME AS "referencedTableName", r.DELETE_RULE AS "deleteAction", r.UPDATE_RULE AS "updateAction", c.IS_DEFERRABLE AS "isDeferrable", c.INITIALLY_DEFERRED AS "initiallyDeferred" FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk ON r.UNIQUE_CONSTRAINT_CATALOG = fk.CONSTRAINT_CATALOG AND r.UNIQUE_CONSTRAINT_SCHEMA = fk.CONSTRAINT_SCHEMA AND r.UNIQUE_CONSTRAINT_NAME = fk.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'PUBLIC' AND c.CONSTRAINT_TYPE = 'FOREIGN KEY' ORDER BY c.CONSTRAINT_NAME`, sqlite3: `SELECT sql FROM sqlite_master WHERE tbl_name = 'myTable'`, + oracle: `SELECT DISTINCT a.table_name "tableName", a.constraint_name "constraintName", a.owner "constraintSchema", a.column_name "columnNames",CASE c.CONSTRAINT_TYPE WHEN 'P' THEN 'PRIMARY KEY' WHEN 'R' THEN 'FOREIGN KEY' WHEN 'C' THEN 'CHECK' WHEN 'U' THEN 'UNIQUE' ELSE NULL END "constraintType", c.r_owner "referencedTableSchema", c.DELETE_RULE "deleteAction", b.table_name "referencedTableName", b.column_name "referencedColumnNames" 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 = 'myTable' AND a.owner = '${dialect.getDefaultSchema()}' ORDER BY a.table_name, a.column_name, b.column_name`, 'mariadb mysql': `SELECT c.CONSTRAINT_SCHEMA AS constraintSchema, c.CONSTRAINT_NAME AS constraintName, c.CONSTRAINT_TYPE AS constraintType, c.TABLE_SCHEMA AS tableSchema, c.TABLE_NAME AS tableName, kcu.COLUMN_NAME AS columnNames, kcu.REFERENCED_TABLE_SCHEMA AS referencedTableSchema, kcu.REFERENCED_TABLE_NAME AS referencedTableName, kcu.REFERENCED_COLUMN_NAME AS referencedColumnNames, r.DELETE_RULE AS deleteAction, r.UPDATE_RULE AS updateAction, ch.CHECK_CLAUSE AS definition FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME AND c.TABLE_NAME = r.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON c.CONSTRAINT_CATALOG = kcu.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND c.TABLE_NAME = kcu.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS ch ON c.CONSTRAINT_CATALOG = ch.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = ch.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = ch.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'sequelize_test' AND c.CONSTRAINT_TYPE = 'FOREIGN KEY' ORDER BY c.CONSTRAINT_NAME, kcu.ORDINAL_POSITION`, }, ); @@ -56,6 +59,7 @@ describe('QueryGenerator#showConstraintsQuery', () => { 'columnName', ]), sqlite3: `SELECT sql FROM sqlite_master WHERE tbl_name = 'myTable'`, + oracle: `SELECT C.CONSTRAINT_NAME "constraintName", CASE A.CONSTRAINT_TYPE WHEN 'P' THEN 'PRIMARY KEY' WHEN 'R' THEN 'FOREIGN KEY' WHEN 'C' THEN 'CHECK' WHEN 'U' THEN 'UNIQUE' ELSE NULL END "constraintType", C.TABLE_NAME "tableName", C.OWNER "constraintSchema", C.COLUMN_NAME "columnNames" FROM ALL_CONS_COLUMNS C INNER JOIN ALL_CONSTRAINTS A ON C.CONSTRAINT_NAME = A.CONSTRAINT_NAME WHERE C.TABLE_NAME ='myTable' AND C.OWNER ='${dialect.getDefaultSchema()}' ORDER BY C.CONSTRAINT_NAME`, 'mariadb mysql': `SELECT c.CONSTRAINT_SCHEMA AS constraintSchema, c.CONSTRAINT_NAME AS constraintName, c.CONSTRAINT_TYPE AS constraintType, c.TABLE_SCHEMA AS tableSchema, c.TABLE_NAME AS tableName, kcu.COLUMN_NAME AS columnNames, kcu.REFERENCED_TABLE_SCHEMA AS referencedTableSchema, kcu.REFERENCED_TABLE_NAME AS referencedTableName, kcu.REFERENCED_COLUMN_NAME AS referencedColumnNames, r.DELETE_RULE AS deleteAction, r.UPDATE_RULE AS updateAction, ch.CHECK_CLAUSE AS definition FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME AND c.TABLE_NAME = r.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON c.CONSTRAINT_CATALOG = kcu.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND c.TABLE_NAME = kcu.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS ch ON c.CONSTRAINT_CATALOG = ch.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = ch.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = ch.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'sequelize_test' AND kcu.COLUMN_NAME = 'some_column' ORDER BY c.CONSTRAINT_NAME, kcu.ORDINAL_POSITION`, }); }); @@ -70,6 +74,7 @@ describe('QueryGenerator#showConstraintsQuery', () => { postgres: `SELECT c.constraint_catalog AS "constraintCatalog", c.constraint_schema AS "constraintSchema", c.constraint_name AS "constraintName", c.constraint_type AS "constraintType", c.table_catalog AS "tableCatalog", c.table_schema AS "tableSchema", c.table_name AS "tableName", kcu.column_name AS "columnNames", ccu.table_schema AS "referencedTableSchema", ccu.table_name AS "referencedTableName", ccu.column_name AS "referencedColumnNames", r.delete_rule AS "deleteAction", r.update_rule AS "updateAction", pg_get_expr(pgc.conbin, pgc.conrelid) AS "definition", c.is_deferrable AS "isDeferrable", c.initially_deferred AS "initiallyDeferred" FROM INFORMATION_SCHEMA.table_constraints c LEFT JOIN INFORMATION_SCHEMA.referential_constraints r ON c.constraint_catalog = r.constraint_catalog AND c.constraint_schema = r.constraint_schema AND c.constraint_name = r.constraint_name LEFT JOIN INFORMATION_SCHEMA.key_column_usage kcu ON c.constraint_catalog = kcu.constraint_catalog AND c.constraint_schema = kcu.constraint_schema AND c.constraint_name = kcu.constraint_name LEFT JOIN information_schema.constraint_column_usage AS ccu ON r.constraint_catalog = ccu.constraint_catalog AND r.constraint_schema = ccu.constraint_schema AND r.constraint_name = ccu.constraint_name LEFT JOIN pg_constraint pgc ON c.constraint_name = pgc.conname AND c.table_schema = (SELECT nspname FROM pg_namespace WHERE oid = pgc.connamespace) AND c.table_name = pgc.conrelid::regclass::text WHERE c.table_name = 'MyModels' AND c.table_schema = 'public' ORDER BY c.constraint_name, kcu.ordinal_position, ccu.column_name`, snowflake: `SELECT c.CONSTRAINT_CATALOG AS "constraintCatalog", c.CONSTRAINT_SCHEMA AS "constraintSchema", c.CONSTRAINT_NAME AS "constraintName", c.CONSTRAINT_TYPE AS "constraintType", c.TABLE_CATALOG AS "tableCatalog", c.TABLE_SCHEMA AS "tableSchema", c.TABLE_NAME AS "tableName", fk.TABLE_SCHEMA AS "referencedTableSchema", fk.TABLE_NAME AS "referencedTableName", r.DELETE_RULE AS "deleteAction", r.UPDATE_RULE AS "updateAction", c.IS_DEFERRABLE AS "isDeferrable", c.INITIALLY_DEFERRED AS "initiallyDeferred" FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk ON r.UNIQUE_CONSTRAINT_CATALOG = fk.CONSTRAINT_CATALOG AND r.UNIQUE_CONSTRAINT_SCHEMA = fk.CONSTRAINT_SCHEMA AND r.UNIQUE_CONSTRAINT_NAME = fk.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'MyModels' AND c.TABLE_SCHEMA = 'PUBLIC' ORDER BY c.CONSTRAINT_NAME`, sqlite3: `SELECT sql FROM sqlite_master WHERE tbl_name = 'MyModels'`, + oracle: `SELECT C.CONSTRAINT_NAME "constraintName", CASE A.CONSTRAINT_TYPE WHEN 'P' THEN 'PRIMARY KEY' WHEN 'R' THEN 'FOREIGN KEY' WHEN 'C' THEN 'CHECK' WHEN 'U' THEN 'UNIQUE' ELSE NULL END "constraintType", C.TABLE_NAME "tableName", C.OWNER "constraintSchema", C.COLUMN_NAME "columnNames" FROM ALL_CONS_COLUMNS C INNER JOIN ALL_CONSTRAINTS A ON C.CONSTRAINT_NAME = A.CONSTRAINT_NAME WHERE C.TABLE_NAME ='MyModels' AND C.OWNER ='${dialect.getDefaultSchema()}' ORDER BY C.CONSTRAINT_NAME`, 'mariadb mysql': `SELECT c.CONSTRAINT_SCHEMA AS constraintSchema, c.CONSTRAINT_NAME AS constraintName, c.CONSTRAINT_TYPE AS constraintType, c.TABLE_SCHEMA AS tableSchema, c.TABLE_NAME AS tableName, kcu.COLUMN_NAME AS columnNames, kcu.REFERENCED_TABLE_SCHEMA AS referencedTableSchema, kcu.REFERENCED_TABLE_NAME AS referencedTableName, kcu.REFERENCED_COLUMN_NAME AS referencedColumnNames, r.DELETE_RULE AS deleteAction, r.UPDATE_RULE AS updateAction, ch.CHECK_CLAUSE AS definition FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME AND c.TABLE_NAME = r.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON c.CONSTRAINT_CATALOG = kcu.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND c.TABLE_NAME = kcu.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS ch ON c.CONSTRAINT_CATALOG = ch.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = ch.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = ch.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'MyModels' AND c.TABLE_SCHEMA = 'sequelize_test' ORDER BY c.CONSTRAINT_NAME, kcu.ORDINAL_POSITION`, }); }); @@ -86,6 +91,7 @@ describe('QueryGenerator#showConstraintsQuery', () => { snowflake: `SELECT c.CONSTRAINT_CATALOG AS "constraintCatalog", c.CONSTRAINT_SCHEMA AS "constraintSchema", c.CONSTRAINT_NAME AS "constraintName", c.CONSTRAINT_TYPE AS "constraintType", c.TABLE_CATALOG AS "tableCatalog", c.TABLE_SCHEMA AS "tableSchema", c.TABLE_NAME AS "tableName", fk.TABLE_SCHEMA AS "referencedTableSchema", fk.TABLE_NAME AS "referencedTableName", r.DELETE_RULE AS "deleteAction", r.UPDATE_RULE AS "updateAction", c.IS_DEFERRABLE AS "isDeferrable", c.INITIALLY_DEFERRED AS "initiallyDeferred" FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk ON r.UNIQUE_CONSTRAINT_CATALOG = fk.CONSTRAINT_CATALOG AND r.UNIQUE_CONSTRAINT_SCHEMA = fk.CONSTRAINT_SCHEMA AND r.UNIQUE_CONSTRAINT_NAME = fk.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'MyModels' AND c.TABLE_SCHEMA = 'PUBLIC' ORDER BY c.CONSTRAINT_NAME`, sqlite3: `SELECT sql FROM sqlite_master WHERE tbl_name = 'MyModels'`, 'mariadb mysql': `SELECT c.CONSTRAINT_SCHEMA AS constraintSchema, c.CONSTRAINT_NAME AS constraintName, c.CONSTRAINT_TYPE AS constraintType, c.TABLE_SCHEMA AS tableSchema, c.TABLE_NAME AS tableName, kcu.COLUMN_NAME AS columnNames, kcu.REFERENCED_TABLE_SCHEMA AS referencedTableSchema, kcu.REFERENCED_TABLE_NAME AS referencedTableName, kcu.REFERENCED_COLUMN_NAME AS referencedColumnNames, r.DELETE_RULE AS deleteAction, r.UPDATE_RULE AS updateAction, ch.CHECK_CLAUSE AS definition FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME AND c.TABLE_NAME = r.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON c.CONSTRAINT_CATALOG = kcu.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND c.TABLE_NAME = kcu.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS ch ON c.CONSTRAINT_CATALOG = ch.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = ch.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = ch.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'MyModels' AND c.TABLE_SCHEMA = 'sequelize_test' ORDER BY c.CONSTRAINT_NAME, kcu.ORDINAL_POSITION`, + oracle: `SELECT C.CONSTRAINT_NAME "constraintName", CASE A.CONSTRAINT_TYPE WHEN 'P' THEN 'PRIMARY KEY' WHEN 'R' THEN 'FOREIGN KEY' WHEN 'C' THEN 'CHECK' WHEN 'U' THEN 'UNIQUE' ELSE NULL END "constraintType", C.TABLE_NAME "tableName", C.OWNER "constraintSchema", C.COLUMN_NAME "columnNames" FROM ALL_CONS_COLUMNS C INNER JOIN ALL_CONSTRAINTS A ON C.CONSTRAINT_NAME = A.CONSTRAINT_NAME WHERE C.TABLE_NAME ='MyModels' AND C.OWNER ='${dialect.getDefaultSchema()}' ORDER BY C.CONSTRAINT_NAME`, }); }); @@ -99,6 +105,7 @@ describe('QueryGenerator#showConstraintsQuery', () => { postgres: `SELECT c.constraint_catalog AS "constraintCatalog", c.constraint_schema AS "constraintSchema", c.constraint_name AS "constraintName", c.constraint_type AS "constraintType", c.table_catalog AS "tableCatalog", c.table_schema AS "tableSchema", c.table_name AS "tableName", kcu.column_name AS "columnNames", ccu.table_schema AS "referencedTableSchema", ccu.table_name AS "referencedTableName", ccu.column_name AS "referencedColumnNames", r.delete_rule AS "deleteAction", r.update_rule AS "updateAction", pg_get_expr(pgc.conbin, pgc.conrelid) AS "definition", c.is_deferrable AS "isDeferrable", c.initially_deferred AS "initiallyDeferred" FROM INFORMATION_SCHEMA.table_constraints c LEFT JOIN INFORMATION_SCHEMA.referential_constraints r ON c.constraint_catalog = r.constraint_catalog AND c.constraint_schema = r.constraint_schema AND c.constraint_name = r.constraint_name LEFT JOIN INFORMATION_SCHEMA.key_column_usage kcu ON c.constraint_catalog = kcu.constraint_catalog AND c.constraint_schema = kcu.constraint_schema AND c.constraint_name = kcu.constraint_name LEFT JOIN information_schema.constraint_column_usage AS ccu ON r.constraint_catalog = ccu.constraint_catalog AND r.constraint_schema = ccu.constraint_schema AND r.constraint_name = ccu.constraint_name LEFT JOIN pg_constraint pgc ON c.constraint_name = pgc.conname AND c.table_schema = (SELECT nspname FROM pg_namespace WHERE oid = pgc.connamespace) AND c.table_name = pgc.conrelid::regclass::text WHERE c.table_name = 'myTable' AND c.table_schema = 'mySchema' ORDER BY c.constraint_name, kcu.ordinal_position, ccu.column_name`, snowflake: `SELECT c.CONSTRAINT_CATALOG AS "constraintCatalog", c.CONSTRAINT_SCHEMA AS "constraintSchema", c.CONSTRAINT_NAME AS "constraintName", c.CONSTRAINT_TYPE AS "constraintType", c.TABLE_CATALOG AS "tableCatalog", c.TABLE_SCHEMA AS "tableSchema", c.TABLE_NAME AS "tableName", fk.TABLE_SCHEMA AS "referencedTableSchema", fk.TABLE_NAME AS "referencedTableName", r.DELETE_RULE AS "deleteAction", r.UPDATE_RULE AS "updateAction", c.IS_DEFERRABLE AS "isDeferrable", c.INITIALLY_DEFERRED AS "initiallyDeferred" FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk ON r.UNIQUE_CONSTRAINT_CATALOG = fk.CONSTRAINT_CATALOG AND r.UNIQUE_CONSTRAINT_SCHEMA = fk.CONSTRAINT_SCHEMA AND r.UNIQUE_CONSTRAINT_NAME = fk.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'mySchema' ORDER BY c.CONSTRAINT_NAME`, sqlite3: `SELECT sql FROM sqlite_master WHERE tbl_name = 'mySchema.myTable'`, + oracle: `SELECT C.CONSTRAINT_NAME "constraintName", CASE A.CONSTRAINT_TYPE WHEN 'P' THEN 'PRIMARY KEY' WHEN 'R' THEN 'FOREIGN KEY' WHEN 'C' THEN 'CHECK' WHEN 'U' THEN 'UNIQUE' ELSE NULL END "constraintType", C.TABLE_NAME "tableName", C.OWNER "constraintSchema", C.COLUMN_NAME "columnNames" FROM ALL_CONS_COLUMNS C INNER JOIN ALL_CONSTRAINTS A ON C.CONSTRAINT_NAME = A.CONSTRAINT_NAME WHERE C.TABLE_NAME ='myTable' AND C.OWNER ='mySchema' ORDER BY C.CONSTRAINT_NAME`, 'mariadb mysql': `SELECT c.CONSTRAINT_SCHEMA AS constraintSchema, c.CONSTRAINT_NAME AS constraintName, c.CONSTRAINT_TYPE AS constraintType, c.TABLE_SCHEMA AS tableSchema, c.TABLE_NAME AS tableName, kcu.COLUMN_NAME AS columnNames, kcu.REFERENCED_TABLE_SCHEMA AS referencedTableSchema, kcu.REFERENCED_TABLE_NAME AS referencedTableName, kcu.REFERENCED_COLUMN_NAME AS referencedColumnNames, r.DELETE_RULE AS deleteAction, r.UPDATE_RULE AS updateAction, ch.CHECK_CLAUSE AS definition FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME AND c.TABLE_NAME = r.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON c.CONSTRAINT_CATALOG = kcu.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND c.TABLE_NAME = kcu.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS ch ON c.CONSTRAINT_CATALOG = ch.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = ch.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = ch.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'mySchema' ORDER BY c.CONSTRAINT_NAME, kcu.ORDINAL_POSITION`, }, ); @@ -118,6 +125,7 @@ describe('QueryGenerator#showConstraintsQuery', () => { postgres: `SELECT c.constraint_catalog AS "constraintCatalog", c.constraint_schema AS "constraintSchema", c.constraint_name AS "constraintName", c.constraint_type AS "constraintType", c.table_catalog AS "tableCatalog", c.table_schema AS "tableSchema", c.table_name AS "tableName", kcu.column_name AS "columnNames", ccu.table_schema AS "referencedTableSchema", ccu.table_name AS "referencedTableName", ccu.column_name AS "referencedColumnNames", r.delete_rule AS "deleteAction", r.update_rule AS "updateAction", pg_get_expr(pgc.conbin, pgc.conrelid) AS "definition", c.is_deferrable AS "isDeferrable", c.initially_deferred AS "initiallyDeferred" FROM INFORMATION_SCHEMA.table_constraints c LEFT JOIN INFORMATION_SCHEMA.referential_constraints r ON c.constraint_catalog = r.constraint_catalog AND c.constraint_schema = r.constraint_schema AND c.constraint_name = r.constraint_name LEFT JOIN INFORMATION_SCHEMA.key_column_usage kcu ON c.constraint_catalog = kcu.constraint_catalog AND c.constraint_schema = kcu.constraint_schema AND c.constraint_name = kcu.constraint_name LEFT JOIN information_schema.constraint_column_usage AS ccu ON r.constraint_catalog = ccu.constraint_catalog AND r.constraint_schema = ccu.constraint_schema AND r.constraint_name = ccu.constraint_name LEFT JOIN pg_constraint pgc ON c.constraint_name = pgc.conname AND c.table_schema = (SELECT nspname FROM pg_namespace WHERE oid = pgc.connamespace) AND c.table_name = pgc.conrelid::regclass::text WHERE c.table_name = 'myTable' AND c.table_schema = 'public' ORDER BY c.constraint_name, kcu.ordinal_position, ccu.column_name`, snowflake: `SELECT c.CONSTRAINT_CATALOG AS "constraintCatalog", c.CONSTRAINT_SCHEMA AS "constraintSchema", c.CONSTRAINT_NAME AS "constraintName", c.CONSTRAINT_TYPE AS "constraintType", c.TABLE_CATALOG AS "tableCatalog", c.TABLE_SCHEMA AS "tableSchema", c.TABLE_NAME AS "tableName", fk.TABLE_SCHEMA AS "referencedTableSchema", fk.TABLE_NAME AS "referencedTableName", r.DELETE_RULE AS "deleteAction", r.UPDATE_RULE AS "updateAction", c.IS_DEFERRABLE AS "isDeferrable", c.INITIALLY_DEFERRED AS "initiallyDeferred" FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk ON r.UNIQUE_CONSTRAINT_CATALOG = fk.CONSTRAINT_CATALOG AND r.UNIQUE_CONSTRAINT_SCHEMA = fk.CONSTRAINT_SCHEMA AND r.UNIQUE_CONSTRAINT_NAME = fk.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'PUBLIC' ORDER BY c.CONSTRAINT_NAME`, sqlite3: `SELECT sql FROM sqlite_master WHERE tbl_name = 'myTable'`, + oracle: `SELECT C.CONSTRAINT_NAME "constraintName", CASE A.CONSTRAINT_TYPE WHEN 'P' THEN 'PRIMARY KEY' WHEN 'R' THEN 'FOREIGN KEY' WHEN 'C' THEN 'CHECK' WHEN 'U' THEN 'UNIQUE' ELSE NULL END "constraintType", C.TABLE_NAME "tableName", C.OWNER "constraintSchema", C.COLUMN_NAME "columnNames" FROM ALL_CONS_COLUMNS C INNER JOIN ALL_CONSTRAINTS A ON C.CONSTRAINT_NAME = A.CONSTRAINT_NAME WHERE C.TABLE_NAME ='myTable' AND C.OWNER ='${dialect.getDefaultSchema()}' ORDER BY C.CONSTRAINT_NAME`, 'mariadb mysql': `SELECT c.CONSTRAINT_SCHEMA AS constraintSchema, c.CONSTRAINT_NAME AS constraintName, c.CONSTRAINT_TYPE AS constraintType, c.TABLE_SCHEMA AS tableSchema, c.TABLE_NAME AS tableName, kcu.COLUMN_NAME AS columnNames, kcu.REFERENCED_TABLE_SCHEMA AS referencedTableSchema, kcu.REFERENCED_TABLE_NAME AS referencedTableName, kcu.REFERENCED_COLUMN_NAME AS referencedColumnNames, r.DELETE_RULE AS deleteAction, r.UPDATE_RULE AS updateAction, ch.CHECK_CLAUSE AS definition FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME AND c.TABLE_NAME = r.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON c.CONSTRAINT_CATALOG = kcu.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND c.TABLE_NAME = kcu.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS ch ON c.CONSTRAINT_CATALOG = ch.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = ch.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = ch.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'sequelize_test' ORDER BY c.CONSTRAINT_NAME, kcu.ORDINAL_POSITION`, }, ); @@ -134,6 +142,7 @@ describe('QueryGenerator#showConstraintsQuery', () => { postgres: `SELECT c.constraint_catalog AS "constraintCatalog", c.constraint_schema AS "constraintSchema", c.constraint_name AS "constraintName", c.constraint_type AS "constraintType", c.table_catalog AS "tableCatalog", c.table_schema AS "tableSchema", c.table_name AS "tableName", kcu.column_name AS "columnNames", ccu.table_schema AS "referencedTableSchema", ccu.table_name AS "referencedTableName", ccu.column_name AS "referencedColumnNames", r.delete_rule AS "deleteAction", r.update_rule AS "updateAction", pg_get_expr(pgc.conbin, pgc.conrelid) AS "definition", c.is_deferrable AS "isDeferrable", c.initially_deferred AS "initiallyDeferred" FROM INFORMATION_SCHEMA.table_constraints c LEFT JOIN INFORMATION_SCHEMA.referential_constraints r ON c.constraint_catalog = r.constraint_catalog AND c.constraint_schema = r.constraint_schema AND c.constraint_name = r.constraint_name LEFT JOIN INFORMATION_SCHEMA.key_column_usage kcu ON c.constraint_catalog = kcu.constraint_catalog AND c.constraint_schema = kcu.constraint_schema AND c.constraint_name = kcu.constraint_name LEFT JOIN information_schema.constraint_column_usage AS ccu ON r.constraint_catalog = ccu.constraint_catalog AND r.constraint_schema = ccu.constraint_schema AND r.constraint_name = ccu.constraint_name LEFT JOIN pg_constraint pgc ON c.constraint_name = pgc.conname AND c.table_schema = (SELECT nspname FROM pg_namespace WHERE oid = pgc.connamespace) AND c.table_name = pgc.conrelid::regclass::text WHERE c.table_name = 'myTable' AND c.table_schema = 'mySchema' ORDER BY c.constraint_name, kcu.ordinal_position, ccu.column_name`, snowflake: `SELECT c.CONSTRAINT_CATALOG AS "constraintCatalog", c.CONSTRAINT_SCHEMA AS "constraintSchema", c.CONSTRAINT_NAME AS "constraintName", c.CONSTRAINT_TYPE AS "constraintType", c.TABLE_CATALOG AS "tableCatalog", c.TABLE_SCHEMA AS "tableSchema", c.TABLE_NAME AS "tableName", fk.TABLE_SCHEMA AS "referencedTableSchema", fk.TABLE_NAME AS "referencedTableName", r.DELETE_RULE AS "deleteAction", r.UPDATE_RULE AS "updateAction", c.IS_DEFERRABLE AS "isDeferrable", c.INITIALLY_DEFERRED AS "initiallyDeferred" FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk ON r.UNIQUE_CONSTRAINT_CATALOG = fk.CONSTRAINT_CATALOG AND r.UNIQUE_CONSTRAINT_SCHEMA = fk.CONSTRAINT_SCHEMA AND r.UNIQUE_CONSTRAINT_NAME = fk.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'mySchema' ORDER BY c.CONSTRAINT_NAME`, sqlite3: `SELECT sql FROM sqlite_master WHERE tbl_name = 'mySchema.myTable'`, + oracle: `SELECT C.CONSTRAINT_NAME "constraintName", CASE A.CONSTRAINT_TYPE WHEN 'P' THEN 'PRIMARY KEY' WHEN 'R' THEN 'FOREIGN KEY' WHEN 'C' THEN 'CHECK' WHEN 'U' THEN 'UNIQUE' ELSE NULL END "constraintType", C.TABLE_NAME "tableName", C.OWNER "constraintSchema", C.COLUMN_NAME "columnNames" FROM ALL_CONS_COLUMNS C INNER JOIN ALL_CONSTRAINTS A ON C.CONSTRAINT_NAME = A.CONSTRAINT_NAME WHERE C.TABLE_NAME ='myTable' AND C.OWNER ='mySchema' ORDER BY C.CONSTRAINT_NAME`, 'mariadb mysql': `SELECT c.CONSTRAINT_SCHEMA AS constraintSchema, c.CONSTRAINT_NAME AS constraintName, c.CONSTRAINT_TYPE AS constraintType, c.TABLE_SCHEMA AS tableSchema, c.TABLE_NAME AS tableName, kcu.COLUMN_NAME AS columnNames, kcu.REFERENCED_TABLE_SCHEMA AS referencedTableSchema, kcu.REFERENCED_TABLE_NAME AS referencedTableName, kcu.REFERENCED_COLUMN_NAME AS referencedColumnNames, r.DELETE_RULE AS deleteAction, r.UPDATE_RULE AS updateAction, ch.CHECK_CLAUSE AS definition FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS c LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS r ON c.CONSTRAINT_CATALOG = r.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = r.CONSTRAINT_NAME AND c.TABLE_NAME = r.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON c.CONSTRAINT_CATALOG = kcu.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND c.TABLE_NAME = kcu.TABLE_NAME LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS ch ON c.CONSTRAINT_CATALOG = ch.CONSTRAINT_CATALOG AND c.CONSTRAINT_SCHEMA = ch.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = ch.CONSTRAINT_NAME WHERE c.TABLE_NAME = 'myTable' AND c.TABLE_SCHEMA = 'mySchema' ORDER BY c.CONSTRAINT_NAME, kcu.ORDINAL_POSITION`, }); }); diff --git a/packages/core/test/unit/query-generator/show-indexes-query.test.ts b/packages/core/test/unit/query-generator/show-indexes-query.test.ts index abdca5b92894..d85318b1f61d 100644 --- a/packages/core/test/unit/query-generator/show-indexes-query.test.ts +++ b/packages/core/test/unit/query-generator/show-indexes-query.test.ts @@ -29,6 +29,11 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = CURRENT SCHEMA and QSYS2.SYSINDEXES.TABLE_NAME = 'myTable'`, + oracle: `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 = 'myTable' AND u.table_owner = '${dialect.getDefaultSchema()}' ORDER BY index_name, column_position`, }); }); @@ -58,6 +63,11 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = CURRENT SCHEMA and QSYS2.SYSINDEXES.TABLE_NAME = 'MyModels'`, + oracle: `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 = 'MyModels' AND u.table_owner = '${dialect.getDefaultSchema()}' ORDER BY index_name, column_position`, }); }); @@ -88,6 +98,9 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = CURRENT SCHEMA and QSYS2.SYSINDEXES.TABLE_NAME = 'MyModels'`, + oracle: `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 = 'MyModels' AND u.table_owner = '${dialect.getDefaultSchema()}' ORDER BY index_name, column_position`, }); }); @@ -115,6 +128,11 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = 'mySchema' and QSYS2.SYSINDEXES.TABLE_NAME = 'myTable'`, + oracle: `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 = 'myTable' AND u.table_owner = 'mySchema' ORDER BY index_name, column_position`, }); }); @@ -148,6 +166,11 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = CURRENT SCHEMA and QSYS2.SYSINDEXES.TABLE_NAME = 'myTable'`, + oracle: `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 = 'myTable' AND u.table_owner = '${dialect.getDefaultSchema()}' ORDER BY index_name, column_position`, }, ); }); @@ -179,6 +202,11 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = 'mySchema' and QSYS2.SYSINDEXES.TABLE_NAME = 'myTable'`, + oracle: `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 = 'myTable' AND u.table_owner = 'mySchema' ORDER BY index_name, column_position`, }); }); diff --git a/packages/core/test/unit/query-generator/start-transaction-query.test.ts b/packages/core/test/unit/query-generator/start-transaction-query.test.ts index 0c23e7ac91b4..3cd417e34c4e 100644 --- a/packages/core/test/unit/query-generator/start-transaction-query.test.ts +++ b/packages/core/test/unit/query-generator/start-transaction-query.test.ts @@ -15,6 +15,7 @@ describe('QueryGenerator#startTransactionQuery', () => { default: 'START TRANSACTION', sqlite3: 'BEGIN DEFERRED TRANSACTION', 'db2 ibmi mssql': notSupportedError, + oracle: 'BEGIN TRANSACTION', }); }); @@ -24,6 +25,7 @@ describe('QueryGenerator#startTransactionQuery', () => { snowflake: 'START TRANSACTION NAME "myTransaction"', sqlite3: 'BEGIN DEFERRED TRANSACTION', 'db2 ibmi mssql': notSupportedError, + oracle: 'BEGIN TRANSACTION', }); }); @@ -86,7 +88,7 @@ describe('QueryGenerator#startTransactionQuery', () => { default: buildInvalidOptionReceivedError('startTransactionQuery', dialect.name, [ 'transactionType', ]), - 'snowflake sqlite3': buildInvalidOptionReceivedError( + 'snowflake sqlite3 oracle': buildInvalidOptionReceivedError( 'startTransactionQuery', dialect.name, ['readOnly'], diff --git a/packages/core/test/unit/query-generator/table-exists-query.test.ts b/packages/core/test/unit/query-generator/table-exists-query.test.ts index a99a4dfba560..c55e3285824a 100644 --- a/packages/core/test/unit/query-generator/table-exists-query.test.ts +++ b/packages/core/test/unit/query-generator/table-exists-query.test.ts @@ -13,6 +13,7 @@ describe('QueryGenerator#tableExistsQuery', () => { ibmi: `SELECT TABLE_NAME FROM QSYS2.SYSTABLES WHERE TABLE_NAME = 'myTable' AND TABLE_SCHEMA = CURRENT SCHEMA`, mssql: `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = N'myTable' AND TABLE_SCHEMA = N'${defaultSchema}'`, sqlite3: `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'myTable'`, + oracle: `SELECT TABLE_NAME FROM ALL_TABLES WHERE TABLE_NAME = 'myTable' AND OWNER = USER`, }); }); @@ -25,6 +26,7 @@ describe('QueryGenerator#tableExistsQuery', () => { ibmi: `SELECT TABLE_NAME FROM QSYS2.SYSTABLES WHERE TABLE_NAME = 'MyModels' AND TABLE_SCHEMA = CURRENT SCHEMA`, mssql: `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = N'MyModels' AND TABLE_SCHEMA = N'${defaultSchema}'`, sqlite3: `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'MyModels'`, + oracle: `SELECT TABLE_NAME FROM ALL_TABLES WHERE TABLE_NAME = 'MyModels' AND OWNER = '${defaultSchema}'`, }); }); @@ -38,6 +40,7 @@ describe('QueryGenerator#tableExistsQuery', () => { ibmi: `SELECT TABLE_NAME FROM QSYS2.SYSTABLES WHERE TABLE_NAME = 'MyModels' AND TABLE_SCHEMA = CURRENT SCHEMA`, mssql: `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = N'MyModels' AND TABLE_SCHEMA = N'${defaultSchema}'`, sqlite3: `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'MyModels'`, + oracle: `SELECT TABLE_NAME FROM ALL_TABLES WHERE TABLE_NAME = 'MyModels' AND OWNER = USER`, }); }); @@ -48,6 +51,7 @@ describe('QueryGenerator#tableExistsQuery', () => { ibmi: `SELECT TABLE_NAME FROM QSYS2.SYSTABLES WHERE TABLE_NAME = 'myTable' AND TABLE_SCHEMA = 'mySchema'`, mssql: `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = N'myTable' AND TABLE_SCHEMA = N'mySchema'`, sqlite3: `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'mySchema.myTable'`, + oracle: `SELECT TABLE_NAME FROM ALL_TABLES WHERE TABLE_NAME = 'myTable' AND OWNER = 'mySchema'`, }); }); @@ -64,6 +68,7 @@ describe('QueryGenerator#tableExistsQuery', () => { ibmi: `SELECT TABLE_NAME FROM QSYS2.SYSTABLES WHERE TABLE_NAME = 'myTable' AND TABLE_SCHEMA = CURRENT SCHEMA`, mssql: `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = N'myTable' AND TABLE_SCHEMA = N'${defaultSchema}'`, sqlite3: `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'myTable'`, + oracle: `SELECT TABLE_NAME FROM ALL_TABLES WHERE TABLE_NAME = 'myTable' AND OWNER = '${defaultSchema}'`, }, ); }); @@ -78,6 +83,7 @@ describe('QueryGenerator#tableExistsQuery', () => { ibmi: `SELECT TABLE_NAME FROM QSYS2.SYSTABLES WHERE TABLE_NAME = 'myTable' AND TABLE_SCHEMA = 'mySchema'`, mssql: `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = N'myTable' AND TABLE_SCHEMA = N'mySchema'`, sqlite3: `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'mySchema.myTable'`, + oracle: `SELECT TABLE_NAME FROM ALL_TABLES WHERE TABLE_NAME = 'myTable' AND OWNER = USER`, }); }); diff --git a/packages/core/test/unit/query-generator/truncate-table-query.test.ts b/packages/core/test/unit/query-generator/truncate-table-query.test.ts index 9dfa1528c1dd..31b1ea03d412 100644 --- a/packages/core/test/unit/query-generator/truncate-table-query.test.ts +++ b/packages/core/test/unit/query-generator/truncate-table-query.test.ts @@ -13,6 +13,7 @@ describe('QueryGenerator#truncateTableQuery', () => { 'db2 ibmi': 'TRUNCATE TABLE "myTable" IMMEDIATE', 'mariadb mysql': 'TRUNCATE `myTable`', 'postgres snowflake': 'TRUNCATE "myTable"', + oracle: `TRUNCATE TABLE "myTable"`, }); }); @@ -62,6 +63,7 @@ describe('QueryGenerator#truncateTableQuery', () => { 'db2 ibmi': 'TRUNCATE TABLE "MyModels" IMMEDIATE', 'mariadb mysql': 'TRUNCATE `MyModels`', 'postgres snowflake': 'TRUNCATE "MyModels"', + oracle: `TRUNCATE TABLE "MyModels"`, }); }); @@ -75,6 +77,7 @@ describe('QueryGenerator#truncateTableQuery', () => { 'db2 ibmi': 'TRUNCATE TABLE "MyModels" IMMEDIATE', 'mariadb mysql': 'TRUNCATE `MyModels`', 'postgres snowflake': 'TRUNCATE "MyModels"', + oracle: `TRUNCATE TABLE "MyModels"`, }); }); @@ -87,6 +90,7 @@ describe('QueryGenerator#truncateTableQuery', () => { 'db2 ibmi': 'TRUNCATE TABLE "mySchema"."myTable" IMMEDIATE', 'mariadb mysql': 'TRUNCATE `mySchema`.`myTable`', 'postgres snowflake': 'TRUNCATE "mySchema"."myTable"', + oracle: 'TRUNCATE TABLE "mySchema"."myTable"', }, ); }); @@ -104,6 +108,7 @@ describe('QueryGenerator#truncateTableQuery', () => { 'db2 ibmi': 'TRUNCATE TABLE "myTable" IMMEDIATE', 'mariadb mysql': 'TRUNCATE `myTable`', 'postgres snowflake': 'TRUNCATE "myTable"', + oracle: `TRUNCATE TABLE "myTable"`, }, ); }); @@ -118,6 +123,7 @@ describe('QueryGenerator#truncateTableQuery', () => { 'db2 ibmi': 'TRUNCATE TABLE "mySchema"."myTable" IMMEDIATE', 'mariadb mysql': 'TRUNCATE `mySchema`.`myTable`', 'postgres snowflake': 'TRUNCATE "mySchema"."myTable"', + oracle: 'TRUNCATE TABLE "mySchema"."myTable"', }); }); diff --git a/packages/core/test/unit/query-generator/update-query.test.ts b/packages/core/test/unit/query-generator/update-query.test.ts index a9397fcd9b43..bad579f86e2c 100644 --- a/packages/core/test/unit/query-generator/update-query.test.ts +++ b/packages/core/test/unit/query-generator/update-query.test.ts @@ -59,6 +59,7 @@ describe('QueryGenerator#updateQuery', () => { expectsql(query, { default: 'UPDATE [Users] SET [firstName]=$sequelize_1,[lastName]=$1,[username]=$sequelize_2', db2: `SELECT * FROM FINAL TABLE (UPDATE "Users" SET "firstName"=$sequelize_1,"lastName"=$1,"username"=$sequelize_2);`, + oracle: `UPDATE "Users" SET "firstName"=:1,"lastName"=$1,"username"=:2`, }); expect(bind).to.deep.eq({ sequelize_1: 'John', @@ -123,6 +124,7 @@ describe('QueryGenerator#updateQuery', () => { query: { default: 'UPDATE [myTable] SET [date]=$sequelize_1 WHERE [id] = $sequelize_2', db2: 'SELECT * FROM FINAL TABLE (UPDATE "myTable" SET "date"=$sequelize_1 WHERE "id" = $sequelize_2);', + oracle: `UPDATE "myTable" SET "date"=:1 WHERE "id" = :2`, }, bind: { mysql: { @@ -157,6 +159,10 @@ describe('QueryGenerator#updateQuery', () => { sequelize_1: '2011-03-27 10:01:55.000 +00:00', sequelize_2: 2, }, + oracle: { + sequelize_1: new Date('2011-03-27T10:01:55Z'), + sequelize_2: 2, + }, }, }); }); @@ -176,6 +182,7 @@ describe('QueryGenerator#updateQuery', () => { default: 'UPDATE [myTable] SET [positive]=$sequelize_1,[negative]=$sequelize_2 WHERE [id] = $sequelize_3', db2: 'SELECT * FROM FINAL TABLE (UPDATE "myTable" SET "positive"=$sequelize_1,"negative"=$sequelize_2 WHERE "id" = $sequelize_3);', + oracle: `UPDATE "myTable" SET "positive"=:1,"negative"=:2 WHERE "id" = :3`, }, bind: { sqlite3: { @@ -218,6 +225,11 @@ describe('QueryGenerator#updateQuery', () => { sequelize_2: false, sequelize_3: 2, }, + oracle: { + sequelize_1: '1', + sequelize_2: '0', + sequelize_3: 2, + }, }, }); }); @@ -237,6 +249,7 @@ describe('QueryGenerator#updateQuery', () => { default: 'UPDATE [myTable] SET [value]=$sequelize_1,[name]=$sequelize_2 WHERE [id] = $sequelize_3', db2: 'SELECT * FROM FINAL TABLE (UPDATE "myTable" SET "value"=$sequelize_1,"name"=$sequelize_2 WHERE "id" = $sequelize_3);', + oracle: `UPDATE "myTable" SET "value"=:1,"name"=:2 WHERE "id" = :3`, }); expect(bind).to.deep.eq({ diff --git a/packages/core/test/unit/query-generator/version-query.test.ts b/packages/core/test/unit/query-generator/version-query.test.ts index 709e962b6934..d93881142f23 100644 --- a/packages/core/test/unit/query-generator/version-query.test.ts +++ b/packages/core/test/unit/query-generator/version-query.test.ts @@ -12,6 +12,7 @@ describe('QueryGenerator#versionQuery', () => { snowflake: 'SELECT CURRENT_VERSION() AS "version"', db2: 'select service_level as "version" from TABLE (sysproc.env_get_inst_info()) as A', ibmi: `SELECT CONCAT(OS_VERSION, CONCAT('.', OS_RELEASE)) AS "version" FROM SYSIBMADM.ENV_SYS_INFO`, + oracle: `SELECT VERSION_FULL FROM PRODUCT_COMPONENT_VERSION WHERE PRODUCT LIKE 'Oracle%'`, }); }); }); diff --git a/packages/core/test/unit/query-interface/bulk-insert.test.ts b/packages/core/test/unit/query-interface/bulk-insert.test.ts index b504d1e0fbf6..e90bece7a6e8 100644 --- a/packages/core/test/unit/query-interface/bulk-insert.test.ts +++ b/packages/core/test/unit/query-interface/bulk-insert.test.ts @@ -41,6 +41,8 @@ describe('QueryInterface#bulkInsert', () => { mssql: toMatchRegex( /^INSERT INTO \[Users\] \(\[firstName\]\) VALUES (?:\(N'\w+'\),){999}\(N'\w+'\);$/, ), + // oracle uses `executeMany()` provided by node-oracledb driver and passes the value with binds + oracle: toMatchRegex(/^INSERT INTO "Users" \("firstName"\) VALUES \(:\d\)$/), }); }); @@ -65,6 +67,7 @@ describe('QueryInterface#bulkInsert', () => { mssql: toMatchRegex( /^(?:INSERT INTO \[Users\] \(\[firstName\]\) VALUES (?:\(N'\w+'\),){999}\(N'\w+'\);){2}$/, ), + oracle: toMatchRegex(/^INSERT INTO "Users" \("firstName"\) VALUES \(:\d\)$/), }); }); @@ -100,6 +103,7 @@ describe('QueryInterface#bulkInsert', () => { ibmi: toMatchSql( `SELECT * FROM FINAL TABLE (INSERT INTO "Users" ("firstName") VALUES (':injection'))`, ), + oracle: toMatchSql(`INSERT INTO "Users" ("firstName") VALUES (:1)`), }); }); }); diff --git a/packages/core/test/unit/query-interface/bulk-update.test.ts b/packages/core/test/unit/query-interface/bulk-update.test.ts index 7fe18bac8103..bf498781c778 100644 --- a/packages/core/test/unit/query-interface/bulk-update.test.ts +++ b/packages/core/test/unit/query-interface/bulk-update.test.ts @@ -48,6 +48,7 @@ describe('QueryInterface#bulkUpdate', () => { expectsql(firstCall.args[0], { default: `UPDATE [Users] SET [firstName]=$sequelize_1 WHERE [firstName] = $sequelize_2`, db2: `SELECT * FROM FINAL TABLE (UPDATE "Users" SET "firstName"=$sequelize_1 WHERE "firstName" = $sequelize_2);`, + oracle: `UPDATE "Users" SET "firstName"=:1 WHERE "firstName" = :2`, }); expect(firstCall.args[1]?.bind).to.deep.eq({ @@ -101,6 +102,7 @@ describe('QueryInterface#bulkUpdate', () => { expectsql(firstCall.args[0], { default: 'UPDATE [Users] SET [firstName]=$sequelize_1 WHERE [firstName] = $one', db2: `SELECT * FROM FINAL TABLE (UPDATE "Users" SET "firstName"=$sequelize_1 WHERE "firstName" = $one);`, + oracle: `UPDATE "Users" SET "firstName"=:1 WHERE "firstName" = $one`, }); expect(firstCall.args[1]?.bind).to.deep.eq({ @@ -132,6 +134,7 @@ describe('QueryInterface#bulkUpdate', () => { expectsql(firstCall.args[0], { default: 'UPDATE [Users] SET [firstName]=$sequelize_1 WHERE [firstName] = $1', db2: `SELECT * FROM FINAL TABLE (UPDATE "Users" SET "firstName"=$sequelize_1 WHERE "firstName" = $1);`, + oracle: `UPDATE "Users" SET "firstName"=:1 WHERE "firstName" = $1`, }); expect(firstCall.args[1]?.bind).to.deep.eq({ diff --git a/packages/core/test/unit/query-interface/create-table.test.ts b/packages/core/test/unit/query-interface/create-table.test.ts index f81d2c7633eb..2aaadeff4a21 100644 --- a/packages/core/test/unit/query-interface/create-table.test.ts +++ b/packages/core/test/unit/query-interface/create-table.test.ts @@ -40,6 +40,7 @@ describe('QueryInterface#createTable', () => { snowflake: 'CREATE TABLE IF NOT EXISTS "table" ("id" VARCHAR(36), PRIMARY KEY ("id"));', db2: 'CREATE TABLE IF NOT EXISTS "table" ("id" CHAR(36) FOR BIT DATA NOT NULL, PRIMARY KEY ("id"));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "table" ("id" CHAR(36), PRIMARY KEY ("id")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "table" ("id" VARCHAR2(36),PRIMARY KEY ("id"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); @@ -93,6 +94,7 @@ describe('QueryInterface#createTable', () => { snowflake: 'CREATE TABLE IF NOT EXISTS "table" ("id" VARCHAR(36), PRIMARY KEY ("id"));', db2: 'CREATE TABLE IF NOT EXISTS "table" ("id" CHAR(36) FOR BIT DATA NOT NULL, PRIMARY KEY ("id"));', ibmi: `BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE VALUE '42710' BEGIN END; CREATE TABLE "table" ("id" CHAR(36), PRIMARY KEY ("id")); END`, + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "table" ("id" VARCHAR2(36),PRIMARY KEY ("id"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); @@ -117,6 +119,8 @@ describe('QueryInterface#createTable', () => { 'mariadb mysql': 'CREATE TABLE IF NOT EXISTS `table` (`json` JSON) ENGINE=InnoDB;', mssql: `IF OBJECT_ID(N'[table]', 'U') IS NULL CREATE TABLE [table] ([json] NVARCHAR(MAX) DEFAULT N'null');`, sqlite3: "CREATE TABLE IF NOT EXISTS `table` (`json` TEXT DEFAULT 'null');", + // oracle uses BLOB with CHECK constraint and JSON_NULL isn't allowed. + oracle: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "table" ("json" BLOB CHECK ("json" IS JSON))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, }); }); }); diff --git a/packages/core/test/unit/query-interface/drop-table.test.ts b/packages/core/test/unit/query-interface/drop-table.test.ts index b1b9261dfd24..1878a1e62a45 100644 --- a/packages/core/test/unit/query-interface/drop-table.test.ts +++ b/packages/core/test/unit/query-interface/drop-table.test.ts @@ -19,6 +19,7 @@ describe('QueryInterface#dropTable', () => { const firstCall = stub.getCall(0); expectsql(firstCall.args[0], { default: 'DROP TABLE IF EXISTS [myTable] CASCADE', + oracle: `BEGIN EXECUTE IMMEDIATE 'DROP TABLE "myTable" CASCADE CONSTRAINTS PURGE'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;`, }); } else { await expect( diff --git a/packages/core/test/unit/query-interface/insert.test.ts b/packages/core/test/unit/query-interface/insert.test.ts index f55b6083a19f..b00251f9e84f 100644 --- a/packages/core/test/unit/query-interface/insert.test.ts +++ b/packages/core/test/unit/query-interface/insert.test.ts @@ -4,6 +4,7 @@ import sinon from 'sinon'; import { beforeAll2, expectsql, sequelize } from '../../support'; describe('QueryInterface#insert', () => { + const dialect = sequelize.dialect; const vars = beforeAll2(() => { const User = sequelize.define( 'User', @@ -22,6 +23,11 @@ describe('QueryInterface#insert', () => { // you'll find more replacement tests in query-generator tests it('does not parse replacements outside of raw sql', async () => { + // Oracle nedds bindDefinitions to be defined for outBinds which can't be obtained with bind and replacement present together. + if (dialect.name === 'oracle') { + return; + } + const { User } = vars; const stub = sinon.stub(sequelize, 'queryRaw'); @@ -77,6 +83,11 @@ describe('QueryInterface#insert', () => { }); it('merges user-provided bind parameters with sequelize-generated bind parameters (object bind)', async () => { + // Oracle doesn't recommend user defined bind. This can mess up the SQL statements leading to errors. + if (dialect.name === 'oracle') { + return; + } + const { User } = vars; const stub = sinon.stub(sequelize, 'queryRaw'); @@ -109,6 +120,10 @@ describe('QueryInterface#insert', () => { }); it('merges user-provided bind parameters with sequelize-generated bind parameters (array bind)', async () => { + if (dialect.name === 'oracle') { + return; + } + const { User } = vars; const stub = sinon.stub(sequelize, 'queryRaw'); diff --git a/packages/core/test/unit/query-interface/raw-select.test.ts b/packages/core/test/unit/query-interface/raw-select.test.ts index 49a3dc1d426d..17c095b4eeb2 100644 --- a/packages/core/test/unit/query-interface/raw-select.test.ts +++ b/packages/core/test/unit/query-interface/raw-select.test.ts @@ -46,6 +46,7 @@ describe('QueryInterface#rawSelect', () => { expectsql(firstCall.args[0], { default: `SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = 'some :data';`, mssql: `SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = N'some :data';`, + oracle: `SELECT "id" FROM "Users" "User" WHERE "User"."username" = 'some :data';`, }); }); }); diff --git a/packages/core/test/unit/query-interface/select.test.ts b/packages/core/test/unit/query-interface/select.test.ts index 32c424bc9583..a804bbafaa10 100644 --- a/packages/core/test/unit/query-interface/select.test.ts +++ b/packages/core/test/unit/query-interface/select.test.ts @@ -41,6 +41,7 @@ describe('QueryInterface#select', () => { expectsql(firstCall.args[0], { default: `SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = 'some :data';`, mssql: `SELECT [id] FROM [Users] AS [User] WHERE [User].[username] = N'some :data';`, + oracle: `SELECT "id" FROM "Users" "User" WHERE "User"."username" = 'some :data';`, }); }); }); diff --git a/packages/core/test/unit/query-interface/update.test.ts b/packages/core/test/unit/query-interface/update.test.ts index 0cb7421f5205..071766eea34c 100644 --- a/packages/core/test/unit/query-interface/update.test.ts +++ b/packages/core/test/unit/query-interface/update.test.ts @@ -52,6 +52,7 @@ describe('QueryInterface#update', () => { mssql: 'UPDATE [Users] SET [firstName]=$sequelize_1 OUTPUT INSERTED.[:data] WHERE [firstName] = $sequelize_2', db2: `SELECT * FROM FINAL TABLE (UPDATE "Users" SET "firstName"=$sequelize_1 WHERE "firstName" = $sequelize_2);`, + oracle: `UPDATE "Users" SET "firstName"=:1 WHERE "firstName" = :2`, }); expect(firstCall.args[1]?.bind).to.deep.eq({ sequelize_1: ':name', @@ -105,6 +106,7 @@ describe('QueryInterface#update', () => { expectsql(firstCall.args[0], { default: 'UPDATE [Users] SET [firstName]=$sequelize_1 WHERE [id] = $id', db2: `SELECT * FROM FINAL TABLE (UPDATE "Users" SET "firstName"=$sequelize_1 WHERE "id" = $id);`, + oracle: `UPDATE "Users" SET "firstName"=:1 WHERE "id" = $id`, }); expect(firstCall.args[1]?.bind).to.deep.eq({ @@ -134,6 +136,7 @@ describe('QueryInterface#update', () => { expectsql(firstCall.args[0], { default: 'UPDATE [Users] SET [firstName]=$sequelize_1 WHERE [id] = $1', db2: `SELECT * FROM FINAL TABLE (UPDATE "Users" SET "firstName"=$sequelize_1 WHERE "id" = $1);`, + oracle: `UPDATE "Users" SET "firstName"=:1 WHERE "id" = $1`, }); expect(firstCall.args[1]?.bind).to.deep.eq({ diff --git a/packages/core/test/unit/query-interface/upsert.test.ts b/packages/core/test/unit/query-interface/upsert.test.ts index 76861783dfd4..498bfa7b80be 100644 --- a/packages/core/test/unit/query-interface/upsert.test.ts +++ b/packages/core/test/unit/query-interface/upsert.test.ts @@ -28,6 +28,11 @@ describe('QueryInterface#upsert', () => { // you'll find more replacement tests in query-generator tests it('does not parse replacements outside of raw sql', async () => { + // For oracle the datatype validation for id fails. Oracle uses Where clause which does the type validation. + if (dialectName === 'oracle') { + return; + } + const { User } = vars; const stub = sinon.stub(sequelize, 'queryRaw'); @@ -53,25 +58,25 @@ describe('QueryInterface#upsert', () => { 'mariadb mysql': 'INSERT INTO `Users` (`firstName`) VALUES ($sequelize_1) ON DUPLICATE KEY UPDATE `firstName`=$sequelize_1;', mssql: ` - MERGE INTO [Users] WITH(HOLDLOCK) - AS [Users_target] - USING (VALUES(N':name')) AS [Users_source]([firstName]) - ON [Users_target].[id] = [Users_source].[id] - WHEN MATCHED THEN - UPDATE SET [Users_target].[firstName] = N':name' - WHEN NOT MATCHED THEN - INSERT ([firstName]) VALUES(N':name') OUTPUT $action, INSERTED.*; - `, + MERGE INTO [Users] WITH(HOLDLOCK) + AS [Users_target] + USING (VALUES(N':name')) AS [Users_source]([firstName]) + ON [Users_target].[id] = [Users_source].[id] + WHEN MATCHED THEN + UPDATE SET [Users_target].[firstName] = N':name' + WHEN NOT MATCHED THEN + INSERT ([firstName]) VALUES(N':name') OUTPUT $action, INSERTED.*; + `, db2: ` - MERGE INTO "Users" - AS "Users_target" - USING (VALUES(':name')) AS "Users_source"("firstName") - ON "Users_target"."id" = "Users_source"."id" - WHEN MATCHED THEN - UPDATE SET "Users_target"."firstName" = ':name' - WHEN NOT MATCHED THEN - INSERT ("firstName") VALUES(':name'); - `, + MERGE INTO "Users" + AS "Users_target" + USING (VALUES(':name')) AS "Users_source"("firstName") + ON "Users_target"."id" = "Users_source"."id" + WHEN MATCHED THEN + UPDATE SET "Users_target"."firstName" = ':name' + WHEN NOT MATCHED THEN + INSERT ("firstName") VALUES(':name'); + `, }); if (dialectName === 'mssql' || dialectName === 'db2') { @@ -106,6 +111,10 @@ describe('QueryInterface#upsert', () => { }); it('merges user-provided bind parameters with sequelize-generated bind parameters (object bind)', async () => { + if (dialectName === 'oracle') { + return; + } + const { User } = vars; const stub = sinon.stub(sequelize, 'queryRaw'); @@ -134,20 +143,20 @@ describe('QueryInterface#upsert', () => { 'mariadb mysql': 'INSERT INTO `Users` (`firstName`,`lastName`) VALUES ($firstName,$sequelize_1) ON DUPLICATE KEY UPDATE `id`=`id`;', mssql: ` - MERGE INTO [Users] WITH(HOLDLOCK) AS [Users_target] - USING (VALUES($firstName, N'Doe')) AS [Users_source]([firstName], [lastName]) - ON [Users_target].[id] = [Users_source].[id] - WHEN NOT MATCHED THEN - INSERT ([firstName], [lastName]) VALUES($firstName, N'Doe') - OUTPUT $action, INSERTED.*; - `, + MERGE INTO [Users] WITH(HOLDLOCK) AS [Users_target] + USING (VALUES($firstName, N'Doe')) AS [Users_source]([firstName], [lastName]) + ON [Users_target].[id] = [Users_source].[id] + WHEN NOT MATCHED THEN + INSERT ([firstName], [lastName]) VALUES($firstName, N'Doe') + OUTPUT $action, INSERTED.*; + `, db2: ` - MERGE INTO "Users" AS "Users_target" - USING (VALUES($firstName, 'Doe')) AS "Users_source"("firstName", "lastName") - ON "Users_target"."id" = "Users_source"."id" - WHEN NOT MATCHED THEN - INSERT ("firstName", "lastName") VALUES($firstName, 'Doe'); - `, + MERGE INTO "Users" AS "Users_target" + USING (VALUES($firstName, 'Doe')) AS "Users_source"("firstName", "lastName") + ON "Users_target"."id" = "Users_source"."id" + WHEN NOT MATCHED THEN + INSERT ("firstName", "lastName") VALUES($firstName, 'Doe'); + `, }); if (dialectName === 'mssql' || dialectName === 'db2') { @@ -163,6 +172,10 @@ describe('QueryInterface#upsert', () => { }); it('merges user-provided bind parameters with sequelize-generated bind parameters (array bind)', async () => { + if (dialectName === 'oracle') { + return; + } + const { User } = vars; const stub = sinon.stub(sequelize, 'queryRaw'); @@ -189,20 +202,20 @@ describe('QueryInterface#upsert', () => { 'mariadb mysql': 'INSERT INTO `Users` (`firstName`,`lastName`) VALUES ($1,$sequelize_1) ON DUPLICATE KEY UPDATE `id`=`id`;', mssql: ` - MERGE INTO [Users] WITH(HOLDLOCK) AS [Users_target] - USING (VALUES($1, N'Doe')) AS [Users_source]([firstName], [lastName]) - ON [Users_target].[id] = [Users_source].[id] - WHEN NOT MATCHED THEN - INSERT ([firstName], [lastName]) VALUES($1, N'Doe') - OUTPUT $action, INSERTED.*; - `, + MERGE INTO [Users] WITH(HOLDLOCK) AS [Users_target] + USING (VALUES($1, N'Doe')) AS [Users_source]([firstName], [lastName]) + ON [Users_target].[id] = [Users_source].[id] + WHEN NOT MATCHED THEN + INSERT ([firstName], [lastName]) VALUES($1, N'Doe') + OUTPUT $action, INSERTED.*; + `, db2: ` - MERGE INTO "Users" AS "Users_target" - USING (VALUES($1, 'Doe')) AS "Users_source"("firstName", "lastName") - ON "Users_target"."id" = "Users_source"."id" - WHEN NOT MATCHED THEN - INSERT ("firstName", "lastName") VALUES($1, 'Doe'); - `, + MERGE INTO "Users" AS "Users_target" + USING (VALUES($1, 'Doe')) AS "Users_source"("firstName", "lastName") + ON "Users_target"."id" = "Users_source"."id" + WHEN NOT MATCHED THEN + INSERT ("firstName", "lastName") VALUES($1, 'Doe'); + `, }); // mssql does not generate any bind parameter @@ -257,6 +270,7 @@ describe('QueryInterface#upsert', () => { ON "Users_target"."id" = "Users_source"."id" WHEN MATCHED THEN UPDATE SET "Users_target"."counter" = \`counter\` + 1 WHEN NOT MATCHED THEN INSERT ("firstName", "counter") VALUES('Jonh', \`counter\` + 1); `, + oracle: `DECLARE BEGIN UPDATE "Users" SET "counter"=\`counter\` + 1; IF (SQL%ROWCOUNT = 0) THEN INSERT INTO "Users" ("firstName","counter") VALUES (:1,\`counter\` + 1); :isUpdate := 0; ELSE :isUpdate := 1; END IF; END;`, }); }); }); diff --git a/packages/core/test/unit/sql/add-column.test.js b/packages/core/test/unit/sql/add-column.test.js index 80fc01bdd300..6d9e6209a6b5 100644 --- a/packages/core/test/unit/sql/add-column.test.js +++ b/packages/core/test/unit/sql/add-column.test.js @@ -115,6 +115,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'ALTER TABLE "custom"."Users" ADD "level_id" REAL NOT NULL;', snowflake: 'ALTER TABLE "custom"."Users" ADD "level_id" FLOAT NOT NULL;', ibmi: 'ALTER TABLE "custom"."Users" ADD "level_id" REAL NOT NULL', + oracle: 'ALTER TABLE "custom"."Users" ADD "level_id" BINARY_FLOAT NOT NULL;', }, ); }); diff --git a/packages/core/test/unit/sql/change-column.test.js b/packages/core/test/unit/sql/change-column.test.js index d96a70a90e37..35a614d4ec05 100644 --- a/packages/core/test/unit/sql/change-column.test.js +++ b/packages/core/test/unit/sql/change-column.test.js @@ -56,6 +56,7 @@ describe('QueryInterface#changeColumn', () => { '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 REAL;', 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;', + 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;`, }); }); @@ -85,6 +86,12 @@ describe('QueryInterface#changeColumn', () => { 'ALTER TABLE "users" ADD FOREIGN KEY ("level_id") REFERENCES "level" ("id") ON DELETE CASCADE ON UPDATE CASCADE;', snowflake: 'ALTER TABLE "users" ADD FOREIGN KEY ("level_id") REFERENCES "level" ("id") ON DELETE CASCADE ON UPDATE CASCADE;', + 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 = '${sequelize.dialect.getDefaultSchema()}' 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;`, }); }); }); diff --git a/packages/core/test/unit/sql/create-table.test.js b/packages/core/test/unit/sql/create-table.test.js index 26b09ddd3b03..7df494826e5b 100644 --- a/packages/core/test/unit/sql/create-table.test.js +++ b/packages/core/test/unit/sql/create-table.test.js @@ -42,6 +42,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { BEGIN END; CREATE TABLE "foo"."users" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) , "mood" VARCHAR(255) CHECK ("mood" IN('happy', 'sad')), PRIMARY KEY ("id")); 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;`, }, ); }); @@ -90,6 +91,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { BEGIN END; CREATE TABLE "bar"."projects" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) , "user_id" INTEGER REFERENCES "bar"."users" ("id") ON DELETE NO ACTION, PRIMARY KEY ("id")); 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;`, }, ); }); @@ -134,6 +136,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { BEGIN END; CREATE TABLE "images" ("id" INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) REFERENCES "files" ("id"), PRIMARY KEY ("id")); 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;`, }, ); }); diff --git a/packages/core/test/unit/sql/generateJoin.test.js b/packages/core/test/unit/sql/generateJoin.test.js index a0b2fb6c58ba..4d5165f01790 100644 --- a/packages/core/test/unit/sql/generateJoin.test.js +++ b/packages/core/test/unit/sql/generateJoin.test.js @@ -122,6 +122,7 @@ describe('QueryGenerator#generateJoin', () => { }, { 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"`, }, ); }); @@ -149,6 +150,7 @@ describe('QueryGenerator#generateJoin', () => { 'INNER JOIN `company` AS `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', + oracle: `INNER JOIN "company" "Company" ON "User"."company_id" = "Company"."id" OR "Company"."public" = 1`, }, ); }); @@ -171,6 +173,7 @@ describe('QueryGenerator#generateJoin', () => { { default: 'LEFT OUTER JOIN [company] AS [Professionals->Company] ON [Professionals].[company_id] = [Professionals->Company].[id]', + oracle: `LEFT OUTER JOIN "company" "Professionals->Company" ON "Professionals"."company_id" = "Professionals->Company"."id"`, }, ); }); @@ -187,6 +190,7 @@ describe('QueryGenerator#generateJoin', () => { }, { default: 'LEFT OUTER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]', + oracle: `LEFT OUTER JOIN "company" "Company" ON "User"."companyId" = "Company"."id"`, }, ); }); @@ -212,6 +216,7 @@ describe('QueryGenerator#generateJoin', () => { "LEFT OUTER JOIN [company] AS [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'", + oracle: `LEFT OUTER JOIN "company" "Company" ON "User"."companyId" = "Company"."id" AND "Company"."name" = 'ABC'`, }, ); }); @@ -233,6 +238,7 @@ describe('QueryGenerator#generateJoin', () => { }, { default: `${sequelize.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"`, }, ); }); @@ -255,6 +261,7 @@ describe('QueryGenerator#generateJoin', () => { { 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"`, }, ); }); @@ -282,6 +289,7 @@ describe('QueryGenerator#generateJoin', () => { { 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"`, }, ); }); @@ -305,6 +313,7 @@ describe('QueryGenerator#generateJoin', () => { { 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"`, }, ); }); @@ -321,6 +330,7 @@ describe('QueryGenerator#generateJoin', () => { }, { default: 'INNER JOIN [company] AS [Company] ON [User].[companyId] = [Company].[id]', + oracle: `INNER JOIN "company" "Company" ON "User"."companyId" = "Company"."id"`, }, ); }); @@ -338,7 +348,10 @@ describe('QueryGenerator#generateJoin', () => { model: User, include: [User.Tasks], }, - { 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"`, + }, ); }); @@ -355,6 +368,7 @@ describe('QueryGenerator#generateJoin', () => { { // 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 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"`, }, ); }); @@ -381,6 +395,7 @@ describe('QueryGenerator#generateJoin', () => { { 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`, }, ); }); @@ -401,6 +416,7 @@ describe('QueryGenerator#generateJoin', () => { }, { 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"`, }, ); }); @@ -433,6 +449,7 @@ describe('QueryGenerator#generateJoin', () => { { 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/packages/core/test/unit/sql/group.test.js b/packages/core/test/unit/sql/group.test.js index f327048c072f..ab575a5c94a5 100644 --- a/packages/core/test/unit/sql/group.test.js +++ b/packages/core/test/unit/sql/group.test.js @@ -42,6 +42,7 @@ describe('QueryGenerator#selectQuery with "group"', () => { ibmi: 'SELECT * FROM "Users" AS "User" GROUP BY "name"', mssql: 'SELECT * FROM [Users] AS [User] GROUP BY [name];', snowflake: 'SELECT * FROM "Users" AS "User" GROUP BY "name";', + oracle: `SELECT * FROM "Users" "User" GROUP BY "name";`, }, ); }); @@ -61,6 +62,7 @@ describe('QueryGenerator#selectQuery with "group"', () => { ibmi: 'SELECT * FROM "Users" AS "User"', mssql: 'SELECT * FROM [Users] AS [User];', snowflake: 'SELECT * FROM "Users" AS "User";', + oracle: `SELECT * FROM "Users" "User";`, }, ); }); diff --git a/packages/core/test/unit/sql/index.test.js b/packages/core/test/unit/sql/index.test.js index c646239e6a2b..d5b8cb1b5366 100644 --- a/packages/core/test/unit/sql/index.test.js +++ b/packages/core/test/unit/sql/index.test.js @@ -89,6 +89,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { postgres: 'CREATE INDEX CONCURRENTLY "user_field_c" ON "User" ("fieldC")', mariadb: 'ALTER TABLE `User` ADD FULLTEXT INDEX `user_field_c` (`fieldC`)', mysql: 'ALTER TABLE `User` ADD FULLTEXT INDEX `user_field_c` (`fieldC`)', + oracle: `CREATE INDEX "user_field_c" ON "User" ("fieldC")`, }, ); @@ -119,6 +120,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { 'ALTER TABLE `User` ADD UNIQUE INDEX `a_b_uniq` USING BTREE (`fieldB`, `fieldA`(5) DESC) WITH PARSER foo', mysql: '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)`, }, ); }); @@ -138,6 +140,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ibmi: 'CREATE INDEX "table_column" ON "table" ("column" DESC)', mariadb: 'ALTER TABLE `table` ADD INDEX `table_column` (`column`(5) DESC)', mysql: 'ALTER TABLE `table` ADD INDEX `table_column` (`column`(5) DESC)', + oracle: `CREATE INDEX "table_column" ON "table" ("column" DESC)`, }, ); }); diff --git a/packages/core/test/unit/sql/insert.test.js b/packages/core/test/unit/sql/insert.test.js index dc0ae9b3d045..4266b67cb25e 100644 --- a/packages/core/test/unit/sql/insert.test.js +++ b/packages/core/test/unit/sql/insert.test.js @@ -45,6 +45,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { 'INSERT INTO "users" ("user_name") VALUES ($sequelize_1) RETURNING "id", "user_name";', db2: 'SELECT * FROM FINAL TABLE (INSERT INTO "users" ("user_name") VALUES ($sequelize_1));', snowflake: 'INSERT INTO "users" ("user_name") VALUES ($sequelize_1);', + oracle: `INSERT INTO "users" ("user_name") VALUES (:1) RETURNING "id", "user_name" INTO :2,:3;`, default: 'INSERT INTO `users` (`user_name`) VALUES ($sequelize_1);', }, bind: { sequelize_1: 'triggertest' }, @@ -69,6 +70,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ibmi: 'SELECT * FROM FINAL TABLE (INSERT INTO "ms" ("id") VALUES ($sequelize_1))', postgres: 'INSERT INTO "ms" ("id") VALUES ($sequelize_1);', snowflake: 'INSERT INTO "ms" ("id") VALUES ($sequelize_1);', + oracle: `INSERT INTO "ms" ("id") VALUES (:1);`, default: 'INSERT INTO `ms` (`id`) VALUES ($sequelize_1);', }, bind: { sequelize_1: 0 }, @@ -170,6 +172,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { { query: { default: 'INSERT INTO [users] ([date]) VALUES ($sequelize_1);', + oracle: `INSERT INTO "users" ("date") VALUES (:1);`, }, bind: { // these dialects change the DB-side timezone, and the input doesn't specify the timezone offset, so we have to offset the value ourselves @@ -179,6 +182,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { mariadb: { sequelize_1: '2015-01-20 01:00:00.000' }, // These dialects do specify the offset, so they can use whichever offset they want. postgres: { sequelize_1: '2015-01-20 01:00:00.000 +01:00' }, + oracle: { sequelize_1: new Date(Date.UTC(2015, 0, 20)) }, }, }, ); @@ -212,6 +216,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'SELECT * FROM FINAL TABLE (INSERT INTO "users" ("date") VALUES ($sequelize_1));', snowflake: 'INSERT INTO "users" ("date") VALUES ($sequelize_1);', mssql: 'INSERT INTO [users] ([date]) VALUES ($sequelize_1);', + oracle: `INSERT INTO "users" ("date") VALUES (:1);`, default: 'INSERT INTO `users` (`date`) VALUES ($sequelize_1);', }, bind: { @@ -223,6 +228,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite3: { sequelize_1: '2015-01-20 00:00:00.000 +00:00' }, mssql: { sequelize_1: '2015-01-20 00:00:00.000 +00:00' }, postgres: { sequelize_1: '2015-01-20 00:00:00.000 +00:00' }, + oracle: { sequelize_1: new Date(Date.UTC(2015, 0, 20)) }, }, }, ); @@ -255,6 +261,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'SELECT * FROM FINAL TABLE (INSERT INTO "users" ("date") VALUES ($sequelize_1));', snowflake: 'INSERT INTO "users" ("date") VALUES ($sequelize_1);', mssql: 'INSERT INTO [users] ([date]) VALUES ($sequelize_1);', + oracle: `INSERT INTO "users" ("date") VALUES (:1);`, default: 'INSERT INTO `users` (`date`) VALUES ($sequelize_1);', }, bind: { @@ -266,6 +273,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { sqlite3: { sequelize_1: '2015-01-20 01:02:03.089 +00:00' }, postgres: { sequelize_1: '2015-01-20 01:02:03.089 +00:00' }, mssql: { sequelize_1: '2015-01-20 01:02:03.089 +00:00' }, + oracle: { sequelize_1: new Date(Date.UTC(2015, 0, 20, 1, 2, 3, 89)) }, }, }, ); @@ -294,6 +302,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'SELECT * FROM FINAL TABLE (INSERT INTO "users" ("user_name") VALUES ($sequelize_1));', snowflake: 'INSERT INTO "users" ("user_name") VALUES ($sequelize_1);', mssql: 'INSERT INTO [users] ([user_name]) VALUES ($sequelize_1);', + oracle: `INSERT INTO "users" ("user_name") VALUES (:1);`, default: 'INSERT INTO `users` (`user_name`) VALUES ($sequelize_1);', }, bind: { @@ -357,11 +366,12 @@ describe(Support.getTestDialectTeaser('SQL'), () => { "INSERT INTO `users` (`user_name`,`pass_word`) VALUES ('testuser','12345') ON DUPLICATE KEY UPDATE `user_name`=VALUES(`user_name`),`pass_word`=VALUES(`pass_word`),`updated_at`=VALUES(`updated_at`);", sqlite3: "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`;", + oracle: `INSERT INTO "users" ("user_name","pass_word") VALUES (:1,:2)`, }, ); }); - it('allow bulk insert primary key with 0', () => { + (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/packages/core/test/unit/sql/literal.test.ts b/packages/core/test/unit/sql/literal.test.ts index 907a2a945c1b..54567e567ace 100644 --- a/packages/core/test/unit/sql/literal.test.ts +++ b/packages/core/test/unit/sql/literal.test.ts @@ -23,6 +23,7 @@ describe('json', () => { sqlite3: `(json_extract(\`metadata\`,'$.language') = '"icelandic"' AND json_extract(\`metadata\`,'$.pg_rating.dk') = '"G"') AND json_extract(\`another_json_field\`,'$.x') = '1'`, mariadb: `(json_compact(json_extract(\`metadata\`,'$.language')) = '"icelandic"' AND json_compact(json_extract(\`metadata\`,'$.pg_rating.dk')) = '"G"') AND json_compact(json_extract(\`another_json_field\`,'$.x')) = '1'`, mysql: `(json_extract(\`metadata\`,'$.language') = CAST('"icelandic"' AS JSON) AND json_extract(\`metadata\`,'$.pg_rating.dk') = CAST('"G"' AS JSON)) AND json_extract(\`another_json_field\`,'$.x') = CAST('1' AS JSON)`, + oracle: `(json_value("metadata",'$."language"') = 'icelandic' AND json_value("metadata",'$."pg_rating"."dk"') = 'G') AND json_value("another_json_field",'$."x"') = '1'`, }); }); @@ -33,6 +34,7 @@ describe('json', () => { postgres: `"metadata"#>ARRAY['pg_rating','dk']::VARCHAR(255)[]`, mariadb: `json_compact(json_extract(\`metadata\`,'$.pg_rating.dk'))`, 'sqlite3 mysql': `json_extract(\`metadata\`,'$.pg_rating.dk')`, + oracle: `json_value("metadata",'$."pg_rating"."dk"')`, }); }); @@ -41,6 +43,7 @@ describe('json', () => { postgres: `"profile"#>ARRAY['id','0','1']::VARCHAR(255)[]`, mariadb: `json_compact(json_extract(\`profile\`,'$.id."0"."1"'))`, 'sqlite3 mysql': `json_extract(\`profile\`,'$.id."0"."1"')`, + oracle: `json_value("profile",'$."id"[0][1]')`, }); }); @@ -53,6 +56,7 @@ describe('json', () => { sqlite3: `json_extract(\`metadata\`,'$.pg_rating.is') = '"U"'`, mariadb: `json_compact(json_extract(\`metadata\`,'$.pg_rating.is')) = '"U"'`, mysql: `json_extract(\`metadata\`,'$.pg_rating.is') = CAST('"U"' AS JSON)`, + oracle: `json_value("metadata",'$."pg_rating"."is"') = 'U'`, }); }); @@ -75,6 +79,7 @@ describe('json', () => { sqlite3: `json_extract(\`profile\`,'$.id') = '1'`, mariadb: `json_compact(json_extract(\`profile\`,'$.id')) = '1'`, mysql: `json_extract(\`profile\`,'$.id') = CAST('1' AS JSON)`, + oracle: `json_value("profile",'$."id"') = '1'`, }); }); @@ -86,6 +91,7 @@ describe('json', () => { sqlite3: `json_extract(\`property\`,'$.value') = '1' AND json_extract(\`another\`,'$.value') = '"string"'`, mariadb: `json_compact(json_extract(\`property\`,'$.value')) = '1' AND json_compact(json_extract(\`another\`,'$.value')) = '"string"'`, mysql: `json_extract(\`property\`,'$.value') = CAST('1' AS JSON) AND json_extract(\`another\`,'$.value') = CAST('"string"' AS JSON)`, + oracle: `json_value("property",'$."value"') = '1' AND json_value("another",'$."value"') = 'string'`, }, ); }); @@ -96,6 +102,7 @@ describe('json', () => { sqlite3: `json_extract(\`profile\`,'$.id') = '"1"'`, mariadb: `json_compact(json_extract(\`profile\`,'$.id')) = '"1"'`, mysql: `json_extract(\`profile\`,'$.id') = CAST('"1"' AS JSON)`, + oracle: `json_value("profile",'$."id"') = '1'`, }); }); }); @@ -154,6 +161,7 @@ describe('fn', () => { mssql: `concat(N'user', 1, 1, N'2011-03-27 10:01:55.000 +00:00', lower(N'user'))`, sqlite3: `concat('user', 1, 1, '2011-03-27 10:01:55.000 +00:00', lower('user'))`, ibmi: `concat('user', 1, 1, '2011-03-27 10:01:55.000', lower('user'))`, + oracle: `concat('user', 1, 1, TO_TIMESTAMP_TZ('2011-03-27 10:01:55.000 +00:00', 'YYYY-MM-DD HH24:MI:SS.FFTZH:TZM'), lower('user'))`, default: `concat('user', 1, true, '2011-03-27 10:01:55.000', lower('user'))`, }); }); diff --git a/packages/core/test/unit/sql/order.test.js b/packages/core/test/unit/sql/order.test.js index aa6f7285eacf..cd20cc643e4c 100644 --- a/packages/core/test/unit/sql/order.test.js +++ b/packages/core/test/unit/sql/order.test.js @@ -336,6 +336,7 @@ describe('QueryGenerator#selectQuery with "order"', () => { '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];', 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";', + 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";`, }, ); }); @@ -358,6 +359,7 @@ describe('QueryGenerator#selectQuery with "order"', () => { postgres: 'SELECT "id", "name" FROM "subtask" AS "Subtask" ORDER BY RANDOM();', snowflake: 'SELECT "id", "name" FROM "subtask" AS "Subtask" ORDER BY RANDOM();', sqlite3: 'SELECT `id`, `name` FROM `subtask` AS `Subtask` ORDER BY RANDOM();', + oracle: `SELECT "id", "name" FROM "subtask" "Subtask" ORDER BY RAND();`, }, ); }); diff --git a/packages/core/test/unit/sql/select.test.js b/packages/core/test/unit/sql/select.test.js index 3d3ce5963dd9..85f9c74e159c 100644 --- a/packages/core/test/unit/sql/select.test.js +++ b/packages/core/test/unit/sql/select.test.js @@ -53,6 +53,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { 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;", ibmi: '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;`, }, ); @@ -80,6 +81,12 @@ describe(Support.getTestDialectTeaser('SQL'), () => { `SELECT * FROM (SELECT [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [User] WHERE [User].[companyId] = 1 ORDER BY [last_name] ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) AS sub`, `SELECT * FROM (SELECT [email], [first_name] AS [firstName], [last_name] AS [lastName] FROM [User] WHERE [User].[companyId] = 5 ORDER BY [last_name] ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) AS sub`, ].join(current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ')}) AS [User];`, + 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 OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub`, + `SELECT * FROM (SELECT "email", "first_name" AS "firstName", "last_name" AS "lastName" FROM "User" WHERE "User"."companyId" = 5 ORDER BY "last_name" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub`, + ].join( + current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ', + )}) "User" ORDER BY "last_name" ASC;`, }, ); @@ -195,6 +202,26 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ].join( current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ', )}) 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_user"."user_id" AS "project_user.userId", "project_user"."project_id" AS "project_user.projectId" + FROM "users" "user" + INNER JOIN "project_users" "project_user" + ON "user"."id_user" = "project_user"."user_id" + AND "project_user"."project_id" = 1 + ORDER BY "subquery_order_0" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY + ) sub`, + `SELECT * FROM ( + SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_user"."user_id" AS "project_user.userId", "project_user"."project_id" AS "project_user.projectId" + FROM "users" "user" + INNER JOIN "project_users" "project_user" + ON "user"."id_user" = "project_user"."user_id" + AND "project_user"."project_id" = 5 + ORDER BY "subquery_order_0" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY + ) sub`, + ].join( + current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ', + )}) "user" ORDER BY "subquery_order_0" ASC;`, }, ); }); @@ -286,6 +313,26 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ].join( current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ', )}) 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_user"."user_id" AS "project_user.userId", "project_user"."project_id" AS "project_user.projectId" + FROM "users" "user" + INNER JOIN "project_users" "project_user" + ON "user"."id_user" = "project_user"."user_id" + AND ("project_user"."project_id" = 1 AND "project_user"."status" = 1) + ORDER BY "subquery_order_0" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY + ) sub`, + `SELECT * FROM ( + SELECT "user"."id_user" AS "id", "user"."last_name" AS "subquery_order_0", "project_user"."user_id" AS "project_user.userId", "project_user"."project_id" AS "project_user.projectId" + FROM "users" "user" + INNER JOIN "project_users" "project_user" + ON "user"."id_user" = "project_user"."user_id" + AND ("project_user"."project_id" = 5 AND "project_user"."status" = 1) + ORDER BY "subquery_order_0" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY + ) sub`, + ].join( + current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ', + )}) "user" ORDER BY "subquery_order_0" ASC;`, }, ); }); @@ -377,6 +424,27 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ].join( current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ', )}) 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_user"."user_id" AS "project_user.userId", "project_user"."project_id" AS "project_user.projectId" + FROM "users" "user" + INNER JOIN "project_users" "project_user" + ON "user"."id_user" = "project_user"."user_id" + AND "project_user"."project_id" = 1 WHERE "user"."age" >= 21 + ORDER BY "subquery_order_0" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY + ) sub`, + `SELECT * FROM ( + SELECT "user"."id_user" AS "id", "user"."id_user" AS "subquery_order_0", "project_user"."user_id" AS "project_user.userId", "project_user"."project_id" AS "project_user.projectId" + FROM "users" "user" + INNER JOIN "project_users" "project_user" + ON "user"."id_user" = "project_user"."user_id" + AND "project_user"."project_id" = 5 + WHERE "user"."age" >= 21 + ORDER BY "subquery_order_0" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY + ) sub`, + ].join( + current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ', + )}) "user" ORDER BY "subquery_order_0" ASC;`, }, ); }); @@ -492,6 +560,12 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ].join( current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ', )}) AS [user] LEFT OUTER JOIN [post] AS [POSTS] ON [user].[id] = [POSTS].[user_id];`, + 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 "lastName" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub`, + `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`, + ].join( + current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ', + )}) "user" LEFT OUTER JOIN "post" "POSTS" ON "user"."id" = "POSTS"."user_id" ORDER BY "lastName" ASC;`, }, ); }); @@ -528,6 +602,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { { 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].${TICK_LEFT}${TICK_LEFT}${TICK_LEFT}last_name${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} ASC LIMIT 30 OFFSET 10) AS [user] LEFT OUTER JOIN [post] AS [POSTS] ON [user].[id_user] = [POSTS].[user_id] ORDER BY [user].${TICK_LEFT}${TICK_LEFT}${TICK_LEFT}last_name${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} ASC;`, 'db2 ibmi mssql': `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].${TICK_LEFT}${TICK_LEFT}${TICK_LEFT}last_name${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} ASC OFFSET 10 ROWS FETCH NEXT 30 ROWS ONLY) AS [user] LEFT OUTER JOIN [post] AS [POSTS] ON [user].[id_user] = [POSTS].[user_id] ORDER BY [user].${TICK_LEFT}${TICK_LEFT}${TICK_LEFT}last_name${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} ASC;`, + 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".${TICK_LEFT}${TICK_LEFT}${TICK_LEFT}last_name${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} 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".${TICK_LEFT}${TICK_LEFT}${TICK_LEFT}last_name${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} ASC;`, }, ); }); @@ -572,6 +647,10 @@ describe(Support.getTestDialectTeaser('SQL'), () => { FROM [users] AS [user] LEFT OUTER JOIN [post] AS [POSTS] ON [user].[id_user] = [POSTS].[user_id] ORDER BY [user].${TICK_LEFT}${TICK_LEFT}${TICK_LEFT}last_name${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} ASC OFFSET 10 ROWS FETCH NEXT 30 ROWS ONLY;`, + 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".${TICK_LEFT}${TICK_LEFT}${TICK_LEFT}last_name${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} ASC OFFSET 10 ROWS FETCH NEXT 30 ROWS ONLY;`, }, ); }); @@ -631,6 +710,12 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ].join( current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ', )}) 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".*, "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 "lastName" ASC OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY) sub`, + `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`, + ].join( + current.dialect.supports['UNION ALL'] ? ' UNION ALL ' : ' UNION ', + )}) "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;`, }, ); }); @@ -679,6 +764,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ), { ibmi: '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";`, 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];', }, @@ -729,6 +815,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ), { 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];`, + oracle: `SELECT "User"."name", "User"."age", "posts"."id" AS "posts.id", "posts"."title" AS "posts.title" FROM "User" "User" ${current.dialect.supports['RIGHT JOIN'] ? 'RIGHT' : 'LEFT'} OUTER JOIN "Post" "posts" ON "User"."id" = "posts"."user_id";`, }, ); }); @@ -803,6 +890,23 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ) ON [user].[id_user] = [projects->project_user].[user_id] ORDER BY [projects->project_user].[user_id] ASC;`, + 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" + ${current.dialect.supports['RIGHT JOIN'] ? 'RIGHT' : 'LEFT'} 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" + ORDER BY "projects->project_user"."user_id" ASC;`, }, ); }); @@ -867,6 +971,11 @@ describe(Support.getTestDialectTeaser('SQL'), () => { '(SELECT [User].[name], [User].[age], [User].[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] ' + `WHERE EXISTS ( SELECT [user_id] FROM [Post] AS [postaliasname] WHERE [postaliasname].[user_id] = [User].[id]) ) AS [User];`, + oracle: + `SELECT "User".* FROM ` + + `(SELECT "User"."name", "User"."age", "User"."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 EXISTS (SELECT "user_id" FROM "Post" "postaliasname" WHERE "postaliasname"."user_id" = "User"."id")) "User";`, }, ); }); @@ -905,6 +1014,11 @@ describe(Support.getTestDialectTeaser('SQL'), () => { '(SELECT [User].[name], [User].[age], [User].[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] ' + `WHERE [postaliasname].[title] = ${sql.escape('test')} AND EXISTS ( SELECT [user_id] FROM [Post] AS [postaliasname] WHERE [postaliasname].[user_id] = [User].[id]) ) AS [User];`, + oracle: + `SELECT "User".* FROM ` + + `(SELECT "User"."name", "User"."age", "User"."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 EXISTS (SELECT "user_id" FROM "Post" "postaliasname" WHERE "postaliasname"."user_id" = "User"."id")) "User";`, }, ); }); @@ -993,6 +1107,16 @@ describe(Support.getTestDialectTeaser('SQL'), () => { 'INNER JOIN [Professions] AS [profession] ON [Users].[professionId] = [profession].[id] ' + `WHERE [Users].[companyId] = [Company].[id] ) ` + `ORDER BY [Company].[id] OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY) AS [Company];`, + oracle: + `SELECT "Company".* FROM (` + + `SELECT "Company"."name", "Company"."public", "Company"."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 EXISTS (` + + `SELECT "Users"."companyId" FROM "Users" "Users" ` + + `INNER JOIN "Professions" "profession" ON "Users"."professionId" = "profession"."id" ` + + `WHERE "Users"."companyId" = "Company"."id"` + + `) ORDER BY "Company"."id" OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY) "Company";`, }, ); }); @@ -1030,6 +1154,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { "SELECT `name`, `age`, `data` FROM `User` AS `User` WHERE `User`.`data` IN (X'313233');", mssql: 'SELECT [name], [age], [data] FROM [User] AS [User] WHERE [User].[data] IN (0x313233);', + oracle: `SELECT "name", "age", "data" FROM "User" "User" WHERE "User"."data" IN ('313233');`, }, ); }); @@ -1184,6 +1309,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { // expectsql fails with consecutive TICKS so we add the dialect-specific one ourself default: `SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[* FROM ${TICK_LEFT}${TICK_LEFT}User${TICK_RIGHT}${TICK_RIGHT}; DELETE FROM ${TICK_LEFT}${TICK_LEFT}User${TICK_RIGHT}${TICK_RIGHT};SELECT ${TICK_LEFT}${TICK_LEFT}id${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} AS ${TICK_LEFT}Posts.* FROM ${TICK_LEFT}${TICK_LEFT}User${TICK_RIGHT}${TICK_RIGHT}; DELETE FROM ${TICK_LEFT}${TICK_LEFT}User${TICK_RIGHT}${TICK_RIGHT};SELECT ${TICK_LEFT}${TICK_LEFT}id${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} FROM ${TICK_LEFT}User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];`, ibmi: '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 ${TICK_LEFT}${TICK_LEFT}User${TICK_RIGHT}${TICK_RIGHT}; DELETE FROM ${TICK_LEFT}${TICK_LEFT}User${TICK_RIGHT}${TICK_RIGHT};SELECT ${TICK_LEFT}${TICK_LEFT}id${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} AS ${TICK_LEFT}Posts.* FROM ${TICK_LEFT}${TICK_LEFT}User${TICK_RIGHT}${TICK_RIGHT}; DELETE FROM ${TICK_LEFT}${TICK_LEFT}User${TICK_RIGHT}${TICK_RIGHT};SELECT ${TICK_LEFT}${TICK_LEFT}id${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} FROM ${TICK_LEFT}User" "User" LEFT OUTER JOIN "Post" "Posts" ON "User"."id" = "Posts"."user_id";`, }, ); @@ -1218,6 +1344,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { // expectsql fails with consecutive TICKS so we add the dialect-specific one ourself default: `SELECT [User].[name], [User].[age], [Posts].[id] AS [Posts.id], [Posts].[* FROM ${TICK_LEFT}${TICK_LEFT}User${TICK_RIGHT}${TICK_RIGHT}; DELETE FROM ${TICK_LEFT}${TICK_LEFT}User${TICK_RIGHT}${TICK_RIGHT};SELECT ${TICK_LEFT}${TICK_LEFT}id${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} AS ${TICK_LEFT}Posts.data] FROM [User] AS [User] LEFT OUTER JOIN [Post] AS [Posts] ON [User].[id] = [Posts].[user_id];`, ibmi: '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"', + oracle: `SELECT "User"."name", "User"."age", "Posts"."id" AS "Posts.id", "Posts"."* FROM ${TICK_LEFT}${TICK_LEFT}User${TICK_RIGHT}${TICK_RIGHT}; DELETE FROM ${TICK_LEFT}${TICK_LEFT}User${TICK_RIGHT}${TICK_RIGHT};SELECT ${TICK_LEFT}${TICK_LEFT}id${TICK_RIGHT}${TICK_RIGHT}${TICK_RIGHT} AS ${TICK_LEFT}Posts.data" FROM "User" "User" LEFT OUTER JOIN "Post" "Posts" ON "User"."id" = "Posts"."user_id";`, }, ); @@ -1241,6 +1368,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ), { ibmi: '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"', + 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];', }, @@ -1275,7 +1403,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { { default: 'SELECT [name], [age] FROM [User];', ibmi: 'SELECT "name", "age" FROM "User"', - postgres: 'SELECT name, age FROM "User";', + 'postgres oracle': 'SELECT name, age FROM "User";', snowflake: 'SELECT name, age FROM User;', }, ); @@ -1331,6 +1459,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { '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;', 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;', + 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;`, }, ); }); @@ -1400,6 +1529,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { '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;', 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;', + 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;`, }, ); }); @@ -1462,6 +1592,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { 'SELECT "User".name, "User".age, "User"."status.label" AS statuslabel, Posts.id AS "Posts.id", Posts.title AS "Posts.title", Posts."status.label" AS "Posts.statuslabel" FROM "User" AS "User" LEFT OUTER JOIN Post AS Posts ON "User".id = Posts.user_id;', snowflake: 'SELECT User.name, User.age, User."status.label" AS statuslabel, Posts.id AS "Posts.id", Posts.title AS "Posts.title", Posts."status.label" AS "Posts.statuslabel" 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" AS statuslabel, Posts.id AS "Posts.id", Posts.title AS "Posts.title", Posts."status.label" AS "Posts.statuslabel" FROM "User" "User" LEFT OUTER JOIN Post Posts ON "User".id = Posts.user_id;`, }, ); }); diff --git a/packages/core/test/unit/sql/update.test.js b/packages/core/test/unit/sql/update.test.js index 461b11d2551a..80606766f96d 100644 --- a/packages/core/test/unit/sql/update.test.js +++ b/packages/core/test/unit/sql/update.test.js @@ -40,6 +40,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { query: { db2: 'SELECT * FROM FINAL TABLE (UPDATE "users" SET "user_name"=$sequelize_1 WHERE "id" = $sequelize_2);', ibmi: 'UPDATE "users" SET "user_name"=$sequelize_1 WHERE "id" = $sequelize_2', + oracle: `UPDATE "users" SET "user_name"=:1 WHERE "id" = :2`, default: 'UPDATE [users] SET [user_name]=$sequelize_1 WHERE [id] = $sequelize_2', }, bind: { @@ -87,6 +88,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { 'UPDATE "users" SET "user_name"=$sequelize_1 WHERE "id" = $sequelize_2 RETURNING "id", "user_name"', db2: 'SELECT * FROM FINAL TABLE (UPDATE "users" SET "user_name"=$sequelize_1 WHERE "id" = $sequelize_2);', snowflake: 'UPDATE "users" SET "user_name"=$sequelize_1 WHERE "id" = $sequelize_2', + oracle: `UPDATE "users" SET "user_name"=:1 WHERE "id" = :2`, default: 'UPDATE `users` SET `user_name`=$sequelize_1 WHERE `id` = $sequelize_2', }, bind: { @@ -133,6 +135,7 @@ describe(Support.getTestDialectTeaser('SQL'), () => { db2: 'SELECT * FROM FINAL TABLE (UPDATE (SELECT * FROM "Users" WHERE "username" = $sequelize_2 FETCH NEXT 1 ROWS ONLY) SET "username"=$sequelize_1);', snowflake: 'UPDATE "Users" SET "username"=$sequelize_1 WHERE "username" = $sequelize_2 LIMIT 1', + oracle: `UPDATE "Users" SET "username"=:1 WHERE "username" = :2 AND rownum <= 1`, default: 'UPDATE [Users] SET [username]=$sequelize_1 WHERE [username] = $sequelize_2', }, bind: { diff --git a/packages/core/test/unit/sql/where.test.ts b/packages/core/test/unit/sql/where.test.ts index 456eb4d96251..1fad6a89551d 100644 --- a/packages/core/test/unit/sql/where.test.ts +++ b/packages/core/test/unit/sql/where.test.ts @@ -412,6 +412,7 @@ Caused by: "undefined" cannot be escaped`), db2: '"stringAttr" = \'here is a null char: \0\'', ibmi: '"stringAttr" = \'here is a null char: \0\'', sqlite3: "`stringAttr` = 'here is a null char: \0'", + oracle: `"stringAttr" = 'here is a null char: \0'`, }, ); @@ -424,6 +425,7 @@ Caused by: "undefined" cannot be escaped`), 'mariadb mysql': `\`dateAttr\` = '2013-01-01 00:00:00.000'`, mssql: `[dateAttr] = N'2013-01-01 00:00:00.000 +00:00'`, 'db2 snowflake ibmi': `"dateAttr" = '2013-01-01 00:00:00.000'`, + oracle: `"dateAttr" = TO_TIMESTAMP_TZ('2013-01-01 00:00:00.000 +00:00', 'YYYY-MM-DD HH24:MI:SS.FFTZH:TZM')`, }, ); @@ -437,6 +439,7 @@ Caused by: "undefined" cannot be escaped`), db2: `"binaryAttr" = BLOB('Sequelize')`, snowflake: `"binaryAttr" = X'53657175656c697a65'`, mssql: '[binaryAttr] = 0x53657175656c697a65', + oracle: `"binaryAttr" = '53657175656c697a65'`, }, ); @@ -451,6 +454,7 @@ Caused by: "undefined" cannot be escaped`), db2: `"binaryAttr" IN (BLOB('Seque''lize1'), BLOB('Sequelize2'))`, snowflake: `"binaryAttr" IN (X'5365717565276c697a6531', X'53657175656c697a6532')`, mssql: '[binaryAttr] IN (0x5365717565276c697a6531, 0x53657175656c697a6532)', + oracle: `"binaryAttr" IN ('5365717565276c697a6531', '53657175656c697a6532')`, }, ); }); @@ -533,7 +537,7 @@ Caused by: "undefined" cannot be escaped`), default: `[booleanAttr] = true`, mssql: '[booleanAttr] = 1', sqlite3: '`booleanAttr` = 1', - ibmi: '"booleanAttr" = 1', + 'ibmi oracle': '"booleanAttr" = 1', }, ); @@ -571,6 +575,7 @@ Caused by: "undefined" cannot be escaped`), mssql: `[dateAttr] = N'2021-01-01 00:00:00.000 +00:00'`, 'mariadb mysql': `\`dateAttr\` = '2021-01-01 00:00:00.000'`, 'db2 ibmi snowflake': `"dateAttr" = '2021-01-01 00:00:00.000'`, + oracle: `"dateAttr" = TO_TIMESTAMP_TZ('2021-01-01 00:00:00.000 +00:00', 'YYYY-MM-DD HH24:MI:SS.FFTZH:TZM')`, }, ); @@ -789,7 +794,7 @@ Caused by: "undefined" cannot be escaped`), { booleanAttr: { [Op.eq]: true } }, { default: '[booleanAttr] = true', - 'mssql sqlite3 ibmi': '[booleanAttr] = 1', + 'mssql sqlite3 ibmi oracle': '[booleanAttr] = 1', }, ); @@ -825,7 +830,7 @@ Caused by: "undefined" cannot be escaped`), { booleanAttr: { [Op.ne]: true } }, { default: '[booleanAttr] != true', - 'mssql ibmi sqlite3': '[booleanAttr] != 1', + 'mssql ibmi sqlite3 oracle': '[booleanAttr] != 1', }, ); @@ -862,7 +867,7 @@ Caused by: "undefined" cannot be escaped`), { booleanAttr: { [Op.is]: false } }, { default: '[booleanAttr] IS false', - 'mssql ibmi sqlite3': '[booleanAttr] IS 0', + 'mssql ibmi sqlite3 oracle': '[booleanAttr] IS 0', }, ); @@ -870,7 +875,7 @@ Caused by: "undefined" cannot be escaped`), { booleanAttr: { [Op.is]: true } }, { default: '[booleanAttr] IS true', - 'mssql ibmi sqlite3': '[booleanAttr] IS 1', + 'mssql ibmi sqlite3 oracle': '[booleanAttr] IS 1', }, ); @@ -966,7 +971,7 @@ Caused by: "undefined" cannot be escaped`), { booleanAttr: { [Op.isNot]: false } }, { default: '[booleanAttr] IS NOT false', - 'mssql ibmi sqlite3': '[booleanAttr] IS NOT 0', + 'mssql ibmi sqlite3 oracle': '[booleanAttr] IS NOT 0', }, ); @@ -974,7 +979,7 @@ Caused by: "undefined" cannot be escaped`), { booleanAttr: { [Op.isNot]: true } }, { default: '[booleanAttr] IS NOT true', - 'mssql ibmi sqlite3': '[booleanAttr] IS NOT 1', + 'mssql ibmi sqlite3 oracle': '[booleanAttr] IS NOT 1', }, ); }); @@ -1024,7 +1029,7 @@ Caused by: "undefined" cannot be escaped`), { default: 'NOT ([booleanAttr] = false)', mssql: 'NOT ([booleanAttr] = 0)', - ibmi: 'NOT ("booleanAttr" = 0)', + 'ibmi oracle': 'NOT ("booleanAttr" = 0)', sqlite3: 'NOT (`booleanAttr` = 0)', }, ); @@ -1034,7 +1039,7 @@ Caused by: "undefined" cannot be escaped`), { default: 'NOT ([booleanAttr] = true)', mssql: 'NOT ([booleanAttr] = 1)', - ibmi: 'NOT ("booleanAttr" = 1)', + 'ibmi oracle': 'NOT ("booleanAttr" = 1)', sqlite3: 'NOT (`booleanAttr` = 1)', }, ); @@ -1088,6 +1093,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `NOT (json_extract(\`data\`,'$.key') = '10')`, mariadb: `NOT (json_compact(json_extract(\`data\`,'$.key')) = '10')`, mysql: `NOT (json_extract(\`data\`,'$.key') = CAST('10' AS JSON))`, + oracle: `NOT (json_value("data",'$."key"') = '10')`, }, ); } @@ -2889,6 +2895,7 @@ Caused by: "undefined" cannot be escaped`), default: `[jsonAttr] = '"value"'`, mysql: `\`jsonAttr\` = CAST('"value"' AS JSON)`, mssql: `[jsonAttr] = N'"value"'`, + oracle: `"jsonAttr" = 'value'`, }, ); @@ -2939,6 +2946,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$.nested') = '"value"'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) = '"value"'`, mysql: `json_extract(\`jsonAttr\`,'$.nested') = CAST('"value"' AS JSON)`, + oracle: `json_value("jsonAttr",'$."nested"') = 'value'`, }, ); @@ -2956,6 +2964,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$.nested') = 'null'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) = 'null'`, mysql: `json_extract(\`jsonAttr\`,'$.nested') = CAST('null' AS JSON)`, + oracle: `json_value("jsonAttr",'$."nested"') = 'null'`, }, ); @@ -2966,6 +2975,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$.nested') IS NULL`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) IS NULL`, mysql: `json_extract(\`jsonAttr\`,'$.nested') IS NULL`, + oracle: `json_value("jsonAttr",'$."nested"') IS NULL`, }, ); @@ -2976,6 +2986,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$.nested') = 'null'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) = 'null'`, mysql: `json_extract(\`jsonAttr\`,'$.nested') = CAST('null' AS JSON)`, + oracle: `json_value("jsonAttr",'$."nested"') = 'null'`, }, ); @@ -2986,6 +2997,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$.nested') IS NULL`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) IS NULL`, mysql: `json_extract(\`jsonAttr\`,'$.nested') IS NULL`, + oracle: `json_value("jsonAttr",'$."nested"') IS NULL`, }, ); @@ -2994,6 +3006,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `'"value"' = json_extract(\`jsonAttr\`,'$.nested')`, mariadb: `'"value"' = json_compact(json_extract(\`jsonAttr\`,'$.nested'))`, mysql: `CAST('"value"' AS JSON) = json_extract(\`jsonAttr\`,'$.nested')`, + oracle: `'value' = json_value("jsonAttr",'$."nested"')`, }); testSql( @@ -3003,6 +3016,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$.nested.twice') = '"value"'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested.twice')) = '"value"'`, mysql: `json_extract(\`jsonAttr\`,'$.nested.twice') = CAST('"value"' AS JSON)`, + oracle: `json_value("jsonAttr",'$."nested"."twice"') = 'value'`, }, ); @@ -3015,6 +3029,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$.nested') = '"value"'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) = '"value"'`, mysql: `json_extract(\`jsonAttr\`,'$.nested') = CAST('"value"' AS JSON)`, + oracle: `json_value("jsonAttr",'$."nested"') = 'value'`, }, ); @@ -3027,6 +3042,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$.nested.twice') = '"value"'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested.twice')) = '"value"'`, mysql: `json_extract(\`jsonAttr\`,'$.nested.twice') = CAST('"value"' AS JSON)`, + oracle: `json_value("jsonAttr",'$."nested"."twice"') = 'value'`, }, ); @@ -3049,6 +3065,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$.nested') != '"value"'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) != '"value"'`, mysql: `json_extract(\`jsonAttr\`,'$.nested') != CAST('"value"' AS JSON)`, + oracle: `json_value("jsonAttr",'$."nested"') != 'value'`, }, ); @@ -3061,6 +3078,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$.nested') = '"value"'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested')) = '"value"'`, mysql: `json_extract(\`jsonAttr\`,'$.nested') = CAST('"value"' AS JSON)`, + oracle: `json_value("jsonAttr",'$."nested"') = 'value'`, }, ); @@ -3073,6 +3091,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`association\`.\`jsonAttr\`,'$.nested') = '"value"'`, mariadb: `json_compact(json_extract(\`association\`.\`jsonAttr\`,'$.nested')) = '"value"'`, mysql: `json_extract(\`association\`.\`jsonAttr\`,'$.nested') = CAST('"value"' AS JSON)`, + oracle: `json_value("association"."jsonAttr",'$."nested"') = 'value'`, }, ); @@ -3085,6 +3104,7 @@ Caused by: "undefined" cannot be escaped`), postgres: `CAST("jsonAttr"->'nested' AS STRING) = 'value'`, mariadb: `CAST(json_compact(json_extract(\`jsonAttr\`,'$.nested')) AS STRING) = 'value'`, 'sqlite3 mysql': `CAST(json_extract(\`jsonAttr\`,'$.nested') AS STRING) = 'value'`, + oracle: `CAST(json_value("jsonAttr",'$."nested"') AS STRING) = 'value'`, }, ); @@ -3107,6 +3127,7 @@ Caused by: "undefined" cannot be escaped`), postgres: `CAST("association"."jsonAttr"#>ARRAY['nested','deep']::VARCHAR(255)[] AS STRING) = 'value'`, mariadb: `CAST(json_compact(json_extract(\`association\`.\`jsonAttr\`,'$.nested.deep')) AS STRING) = 'value'`, 'sqlite3 mysql': `CAST(json_extract(\`association\`.\`jsonAttr\`,'$.nested.deep') AS STRING) = 'value'`, + oracle: `CAST(json_value("association"."jsonAttr",'$."nested"."deep"') AS STRING) = 'value'`, }, ); @@ -3118,6 +3139,7 @@ Caused by: "undefined" cannot be escaped`), postgres: `CAST("jsonAttr"->'nested' AS STRING) = 'value'`, mariadb: `CAST(json_compact(json_extract(\`jsonAttr\`,'$.nested')) AS STRING) = 'value'`, 'sqlite3 mysql': `CAST(json_extract(\`jsonAttr\`,'$.nested') AS STRING) = 'value'`, + oracle: `CAST(json_value("jsonAttr",'$."nested"') AS STRING) = 'value'`, }, ); @@ -3128,6 +3150,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$.nested.attribute') = '4'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$.nested.attribute')) = '4'`, mysql: `json_extract(\`jsonAttr\`,'$.nested.attribute') = CAST('4' AS JSON)`, + oracle: `json_value("jsonAttr",'$."nested"."attribute"') = '4'`, }, ); @@ -3139,6 +3162,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$."0"') = '4'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$."0"')) = '4'`, mysql: `json_extract(\`jsonAttr\`,'$."0"') = CAST('4' AS JSON)`, + oracle: `json_value("jsonAttr",'$[0]') = '4'`, }, ); @@ -3152,6 +3176,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$[0]') = '4'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$[0]')) = '4'`, mysql: `json_extract(\`jsonAttr\`,'$[0]') = CAST('4' AS JSON)`, + oracle: `json_value("jsonAttr",'$[0]') = '4'`, }, ); @@ -3162,6 +3187,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$."0".attribute') = '4'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$."0".attribute')) = '4'`, mysql: `json_extract(\`jsonAttr\`,'$."0".attribute') = CAST('4' AS JSON)`, + oracle: `json_value("jsonAttr",'$[0]."attribute"') = '4'`, }, ); @@ -3173,6 +3199,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$."hyphenated-key"') = '4'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$."hyphenated-key"')) = '4'`, mysql: `json_extract(\`jsonAttr\`,'$."hyphenated-key"') = CAST('4' AS JSON)`, + oracle: `json_value("jsonAttr",'$."hyphenated-key"') = '4'`, }, ); @@ -3184,6 +3211,7 @@ Caused by: "undefined" cannot be escaped`), mysql: `json_extract(\`jsonAttr\`,'$."a\\')) AS DECIMAL) = 1 DELETE YOLO INJECTIONS; -- "') = CAST('1' AS JSON)`, sqlite3: `json_extract(\`jsonAttr\`,'$."a'')) AS DECIMAL) = 1 DELETE YOLO INJECTIONS; -- "') = '1'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$."a\\')) AS DECIMAL) = 1 DELETE YOLO INJECTIONS; -- "')) = '1'`, + oracle: `json_value("jsonAttr",'$."a'')) AS DECIMAL) = 1 DELETE YOLO INJECTIONS; -- "') = '1'`, }, ); @@ -3196,6 +3224,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`jsonAttr\`,'$[0].nested.attribute') = '4'`, mariadb: `json_compact(json_extract(\`jsonAttr\`,'$[0].nested.attribute')) = '4'`, mysql: `json_extract(\`jsonAttr\`,'$[0].nested.attribute') = CAST('4' AS JSON)`, + oracle: `json_value("jsonAttr",'$[0]."nested"."attribute"') = '4'`, }, ); @@ -3207,6 +3236,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`aliased_json\`,'$.nested.attribute') = '4'`, mariadb: `json_compact(json_extract(\`aliased_json\`,'$.nested.attribute')) = '4'`, mysql: `json_extract(\`aliased_json\`,'$.nested.attribute') = CAST('4' AS JSON)`, + oracle: `json_value("aliased_json",'$."nested"."attribute"') = '4'`, }, ); } @@ -4002,6 +4032,7 @@ Caused by: "undefined" cannot be escaped`), sqlite3: `json_extract(\`col\`,'$.jsonPath') = '"value"'`, mariadb: `json_compact(json_extract(\`col\`,'$.jsonPath')) = '"value"'`, mysql: `json_extract(\`col\`,'$.jsonPath') = CAST('"value"' AS JSON)`, + oracle: `json_value("col",'$."jsonPath"') = 'value'`, }); } }); diff --git a/packages/core/test/unit/transaction.test.ts b/packages/core/test/unit/transaction.test.ts index f181001af030..f42c6b8e19d9 100644 --- a/packages/core/test/unit/transaction.test.ts +++ b/packages/core/test/unit/transaction.test.ts @@ -51,6 +51,7 @@ describe('Transaction', () => { all: ['START TRANSACTION'], snowflake: ['START TRANSACTION NAME "123"'], sqlite3: ['BEGIN DEFERRED TRANSACTION'], + oracle: ['BEGIN TRANSACTION'], }; await sequelize.transaction(async () => { @@ -65,6 +66,7 @@ describe('Transaction', () => { all: ['SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED', 'START TRANSACTION'], postgres: ['START TRANSACTION', 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED'], sqlite3: ['BEGIN DEFERRED TRANSACTION', 'PRAGMA read_uncommitted = 1'], + oracle: ['BEGIN TRANSACTION', 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED'], }; try { diff --git a/packages/core/test/unit/utils/sql.test.ts b/packages/core/test/unit/utils/sql.test.ts index bfb1bd45915a..966ab7b2ae11 100644 --- a/packages/core/test/unit/utils/sql.test.ts +++ b/packages/core/test/unit/utils/sql.test.ts @@ -18,7 +18,7 @@ const { list } = sqlTag; const dialect = sequelize.dialect; -const supportsNamedParameters = dialect.name === 'sqlite3' || dialect.name === 'mssql'; +const supportsNamedParameters = ['sqlite3', 'mssql', 'oracle'].includes(dialect.name); describe('mapBindParameters', () => { it('parses named bind parameters', () => { @@ -32,6 +32,7 @@ describe('mapBindParameters', () => { postgres: `SELECT "$id" FROM users WHERE id = '$id' OR id = $1 OR id = '''$id'''`, sqlite3: `SELECT \`$id\` FROM users WHERE id = '$id' OR id = $id OR id = '''$id'''`, mssql: `SELECT [$id] FROM users WHERE id = '$id' OR id = @id OR id = '''$id'''`, + oracle: `SELECT "$id" FROM users WHERE id = '$id' OR id = :id OR id = '''$id'''`, }); if (supportsNamedParameters) { @@ -52,6 +53,7 @@ describe('mapBindParameters', () => { postgres: `SELECT * FROM users WHERE id = $1`, sqlite3: `SELECT * FROM users WHERE id = $1`, mssql: `SELECT * FROM users WHERE id = @1`, + oracle: `SELECT * FROM users WHERE id = :1`, }); if (supportsNamedParameters) { @@ -71,6 +73,7 @@ describe('mapBindParameters', () => { postgres: `SELECT * FROM users WHERE id = $1::string`, sqlite3: `SELECT * FROM users WHERE id = $param::string`, mssql: `SELECT * FROM users WHERE id = @param::string`, + oracle: `SELECT * FROM users WHERE id = :param::string`, }); }); @@ -82,6 +85,7 @@ describe('mapBindParameters', () => { postgres: `SELECT * FROM users WHERE json_col->>$1`, sqlite3: `SELECT * FROM users WHERE json_col->>$key`, mssql: `SELECT * FROM users WHERE json_col->>@key`, + oracle: `SELECT * FROM users WHERE json_col->>:key`, }); }); @@ -94,6 +98,7 @@ describe('mapBindParameters', () => { sqlite3: `SELECT * FROM users WHERE id = $id;`, mssql: `SELECT * FROM users WHERE id = @id;`, ibmi: `SELECT * FROM users WHERE id = ?;`, // 'default' removes the ; for ibmi + oracle: `SELECT * FROM users WHERE id = :id;`, }); }); @@ -121,6 +126,7 @@ describe('mapBindParameters', () => { postgres: `SELECT * FROM users WHERE id = $1`, sqlite3: `SELECT * FROM users WHERE id = $a`, mssql: `SELECT * FROM users WHERE id = @a`, + oracle: `SELECT * FROM users WHERE id = :a`, }); if (supportsNamedParameters) { @@ -143,6 +149,7 @@ describe('mapBindParameters', () => { postgres: `SELECT * FROM users WHERE id = fn($1) OR id = fn('a',$1) OR id=$1 OR id$id = 1 OR id = $1`, sqlite3: `SELECT * FROM users WHERE id = fn($id) OR id = fn('a',$id) OR id=$id OR id$id = 1 OR id = $id`, mssql: `SELECT * FROM users WHERE id = fn(@id) OR id = fn('a',@id) OR id=@id OR id$id = 1 OR id = @id`, + oracle: `SELECT * FROM users WHERE id = fn(:id) OR id = fn('a',:id) OR id=:id OR id$id = 1 OR id = :id`, }); if (supportsNamedParameters) { @@ -166,6 +173,8 @@ describe('mapBindParameters', () => { if (supportsNamedParameters) { expect(bindOrder).to.be.null; + } else if (dialect.name === 'oracle') { + expect(bindOrder).to.be.null; } else { expect(bindOrder).to.deep.eq([]); } @@ -196,6 +205,7 @@ describe('mapBindParameters', () => { postgres: `SELECT z$$ $1 x$$ * FROM users`, sqlite3: `SELECT z$$ $id x$$ * FROM users`, mssql: `SELECT z$$ @id x$$ * FROM users`, + oracle: `SELECT z$$ :id x$$ * FROM users`, }); if (supportsNamedParameters) { @@ -216,6 +226,7 @@ describe('mapBindParameters', () => { postgres: `SELECT $$ abc $$ AS string FROM users WHERE id = $1`, sqlite3: `SELECT $$ abc $$ AS string FROM users WHERE id = $id`, mssql: `SELECT $$ abc $$ AS string FROM users WHERE id = @id`, + oracle: `SELECT $$ abc $$ AS string FROM users WHERE id = :id`, }); if (supportsNamedParameters) { @@ -316,6 +327,7 @@ SELECT * FROM users WHERE id = e'\\' $id' OR id = $id`), postgres: `SELECT * FROM users WHERE id = '\\\\' OR id = $1`, sqlite3: `SELECT * FROM users WHERE id = '\\\\' OR id = $id`, mssql: `SELECT * FROM users WHERE id = '\\\\' OR id = @id`, + oracle: `SELECT * FROM users WHERE id = '\\\\' OR id = :id`, }); if (supportsNamedParameters) { @@ -366,6 +378,10 @@ SELECT * FROM users WHERE id = '\\\\\\' $id' OR id = $id`), SELECT * FROM users -- WHERE id = $id WHERE id = @id `, + oracle: ` + SELECT * FROM users -- WHERE id = $id + WHERE id = :id + `, }); }); @@ -422,6 +438,12 @@ SELECT * FROM users WHERE id = '\\\\\\' $id' OR id = $id`), */ WHERE id = @id `, + oracle: ` + SELECT * FROM users /* + WHERE id = $id + */ + WHERE id = :id + `, }); }); }); diff --git a/packages/oracle/.eslintrc.js b/packages/oracle/.eslintrc.js new file mode 100644 index 000000000000..e13dec291282 --- /dev/null +++ b/packages/oracle/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + parserOptions: { + project: [`${__dirname}/tsconfig.json`], + }, +}; diff --git a/packages/oracle/package.json b/packages/oracle/package.json new file mode 100644 index 000000000000..67ae5f42e96b --- /dev/null +++ b/packages/oracle/package.json @@ -0,0 +1,53 @@ +{ + "bugs": "https://github.com/sequelize/sequelize/issues", + "description": "Oracle Database Connector for Sequelize", + "exports": { + ".": { + "import": { + "types": "./lib/index.d.mts", + "default": "./lib/index.mjs" + }, + "require": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + } + } + }, + "files": [ + "lib" + ], + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "sideEffects": false, + "homepage": "https://sequelize.org", + "license": "MIT", + "name": "@sequelize/oracle", + "repository": "https://github.com/sequelize/sequelize", + "scripts": { + "build": "../../build-packages.mjs oracle", + "test": "concurrently \"npm:test-*\"", + "test-typings": "tsc --noEmit --project tsconfig.json", + "test-exports": "../../dev/sync-exports.mjs ./src --check-outdated", + "sync-exports": "../../dev/sync-exports.mjs ./src" + }, + "type": "commonjs", + "version": "7.0.0-alpha.46", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sequelize/core": "workspace:*", + "@sequelize/utils": "workspace:*", + "@types/oracledb": "^6.6.0", + "dayjs": "^1.11.18", + "lodash": "^4.17.21", + "oracledb": "^6.8.0", + "semver": "^7.7.1" + }, + "devDependencies": { + "@types/chai": "4.3.20", + "@types/mocha": "10.0.10", + "chai": "4.5.0", + "mocha": "11.7.1" + } +} diff --git a/packages/oracle/src/_internal/connection-options.ts b/packages/oracle/src/_internal/connection-options.ts new file mode 100644 index 000000000000..d54448bfc496 --- /dev/null +++ b/packages/oracle/src/_internal/connection-options.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +import type { PickByType } from '@sequelize/utils'; +import { getSynchronizedTypeKeys } from '@sequelize/utils'; +import type { OracleConnectionOptions } from '../connection-manager.js'; + +type StringConnectionOptions = PickByType; + +const STRING_CONNECTION_OPTION_MAP = { + configDir: undefined, + connectionIdPrefix: undefined, + connectString: undefined, + database: undefined, + debugJdwp: undefined, + edition: undefined, + host: undefined, + httpsProxy: undefined, + newPassword: undefined, + password: undefined, + poolAlias: undefined, + port: undefined, + sourceRoute: undefined, + sslServerCertDN: undefined, + tag: undefined, + username: undefined, + walletPassword: undefined, + walletLocation: undefined, +} as const satisfies Record; + +export const STRING_CONNECTION_OPTION_NAMES = getSynchronizedTypeKeys( + STRING_CONNECTION_OPTION_MAP, +); + +type BooleanConnectionOptions = PickByType; + +const BOOLEAN_CONNECTION_OPTION_MAP = { + events: undefined, + externalAuth: undefined, + matchAny: undefined, + sslAllowWeakDNMatch: undefined, + sslServerDNMatch: undefined, +} as const satisfies Record; + +export const BOOLEAN_CONNECTION_OPTION_NAMES = getSynchronizedTypeKeys( + BOOLEAN_CONNECTION_OPTION_MAP, +); + +type NumberConnectionOptions = PickByType; + +const NUMBER_CONNECTION_OPTION_MAP = { + connectTimeout: undefined, + expireTime: undefined, + httpsProxyPort: undefined, + port: undefined, + privilege: undefined, + retryCount: undefined, + retryDelay: undefined, + sdu: undefined, + stmtCacheSize: undefined, + transportConnectTimeout: undefined, +} as const satisfies Record; + +export const NUMBER_CONNECTION_OPTION_NAMES = getSynchronizedTypeKeys( + NUMBER_CONNECTION_OPTION_MAP, +); + +export const CONNECTION_OPTION_NAMES = getSynchronizedTypeKeys({ + ...STRING_CONNECTION_OPTION_MAP, + ...BOOLEAN_CONNECTION_OPTION_MAP, + ...NUMBER_CONNECTION_OPTION_MAP, + accessToken: undefined, + accessTokenConfig: undefined, + shardingKey: undefined, + superShardingKey: undefined, +}); diff --git a/packages/oracle/src/_internal/data-types-overrides.ts b/packages/oracle/src/_internal/data-types-overrides.ts new file mode 100644 index 000000000000..0df33ee828fb --- /dev/null +++ b/packages/oracle/src/_internal/data-types-overrides.ts @@ -0,0 +1,419 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +import type { AbstractDialect, BindParamOptions } from '@sequelize/core'; +import type { AcceptedDate } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types.js'; +import * as BaseTypes from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types.js'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; + +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +type Lib = typeof import('oracledb'); + +dayjs.extend(utc); + +// legacy support +let Moment: any; +try { + // eslint-disable-next-line import/no-extraneous-dependencies + Moment = require('moment'); +} catch { + /* ignore */ +} + +function isMoment(value: any): boolean { + return Moment?.isMoment(value) ?? false; +} + +export class STRING extends BaseTypes.STRING { + protected _checkOptionSupport(dialect: AbstractDialect) { + super._checkOptionSupport(dialect); + // @ts-expect-error -- Object is possibly 'null'. + if (this.options.length > 4000 || (this.options.binary && this.options.length > 2000)) { + dialect.warnDataTypeIssue( + `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`, + ); + } + } + + toSql() { + if (!this.options.binary) { + return `NVARCHAR2(${this.options.length ?? 255})`; + } + + return `RAW(${this.options.length ?? 255})`; + } + + _getBindDef(oracledb: Lib) { + if (this.options.binary) { + return { type: oracledb.DB_TYPE_RAW, maxSize: this.options.length || 255 }; + } + + return { type: oracledb.DB_TYPE_VARCHAR, maxSize: this.options.length || 255 }; + } +} + +export class BOOLEAN extends BaseTypes.BOOLEAN { + toSql() { + return 'CHAR(1)'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_CHAR, maxSize: 1 }; + } + + escape(value: boolean): string { + return value ? '1' : '0'; + } + + toBindableValue(value: boolean): unknown { + return value === true ? '1' : value === false ? '0' : value; + } + + parseDatabaseValue(value: unknown): boolean { + if (value === '1' || value === 'true') { + return true; + } + + return false; + } +} + +export class UUID extends BaseTypes.UUID { + toSql() { + return 'VARCHAR2(36)'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_VARCHAR, maxSize: 36 }; + } +} + +export class NOW extends BaseTypes.NOW { + toSql(): string { + return 'SYSDATE'; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + toBindableValue(value: never): unknown { + return 'SYSDATE'; + } +} + +export class ENUM extends BaseTypes.ENUM { + toSql() { + return 'VARCHAR2(512)'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_VARCHAR, maxSize: 512 }; + } +} + +export class TEXT extends BaseTypes.TEXT { + toSql() { + return 'CLOB'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_CLOB }; + } +} + +export class CHAR extends BaseTypes.CHAR { + protected _checkOptionSupport(dialect: AbstractDialect) { + super._checkOptionSupport(dialect); + if (this.options.binary) { + dialect.warnDataTypeIssue('Oracle CHAR.BINARY datatype is not of Fixed Length.'); + } + } + + toSql() { + if (this.options.binary) { + return `RAW(${this.options.length ?? 255})`; + } + + return super.toSql(); + } + + _getBindDef(oracledb: Lib) { + if (this.options.binary) { + return { type: oracledb.DB_TYPE_RAW, maxSize: this.options.length }; + } + + return { type: oracledb.DB_TYPE_CHAR, maxSize: this.options.length }; + } +} + +export class DATE extends BaseTypes.DATE { + toSql() { + return 'TIMESTAMP WITH LOCAL TIME ZONE'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_TIMESTAMP_LTZ }; + } + + toBindableValue(date: AcceptedDate) { + const format = 'YYYY-MM-DD HH24:MI:SS.FFTZH:TZM'; + date = this._applyTimezone(date); + + const formatedDate = date.format('YYYY-MM-DD HH:mm:ss.SSS Z'); + + return `TO_TIMESTAMP_TZ('${formatedDate}', '${format}')`; + } + + /** + * avoids appending TO_TIMESTAMP_TZ in toBindableValue() + * + * @override + */ + getBindParamSql(value: AcceptedDate, options: BindParamOptions): string { + if (dayjs.isDayjs(value) || isMoment(value)) { + return options.bindParam(this._sanitize(value)); + } + + return options.bindParam(value); + } + + _sanitize(value: any) { + return new Date(value); + } +} + +type AcceptedNumber = number | bigint | boolean | string | null; + +export class DECIMAL extends BaseTypes.DECIMAL { + toSql() { + let result: string = 'NUMBER'; + if (!this.options.precision) { + return result; + } + + result += `(${this.options.precision}`; + + if (this.options.scale) { + result += `, ${this.options.scale}`; + } + + result += ')'; + + return result; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_NUMBER }; + } + + // Oracle treats DECIMAL as NUMBER(precision, scale). + sanitize(value: AcceptedNumber): AcceptedNumber { + if (typeof value === 'bigint') { + return value.toString(); + } + + return value; + } +} + +export class TINYINT extends BaseTypes.TINYINT { + toSql() { + return 'NUMBER(3)'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_NUMBER }; + } +} + +export class SMALLINT extends BaseTypes.SMALLINT { + toSql() { + if (this.options.length) { + return `NUMBER(${this.options.length},0)`; + } + + return 'SMALLINT'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_NUMBER }; + } +} + +export class MEDIUMINT extends BaseTypes.MEDIUMINT { + toSql() { + return 'NUMBER(8)'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_NUMBER }; + } +} + +export class INTEGER extends BaseTypes.INTEGER { + toSql(): string { + if (this.options.length) { + return `NUMBER(${this.options.length},0)`; + } + + return 'INTEGER'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_NUMBER }; + } +} + +/** + * @deprecated use FLOAT. + */ +export class REAL extends BaseTypes.REAL { + toSql() { + return 'BINARY_DOUBLE'; + } + + // https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=GUID-0BA2E065-8006-426C-A3CB-1F6B0C8F283C + toBindableValue(value: any) { + if (value === Number.POSITIVE_INFINITY) { + return 'inf'; + } + + if (value === Number.NEGATIVE_INFINITY) { + return '-inf'; + } + + return value; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_BINARY_DOUBLE }; + } +} + +export class BIGINT extends BaseTypes.BIGINT { + protected _checkOptionSupport(dialect: AbstractDialect) { + super._checkOptionSupport(dialect); + if (this.options.length || this.options.zerofill) { + dialect.warnDataTypeIssue(`${dialect.name} does not support BIGINT with options.`); + delete this.options.length; + this.options.zerofill = undefined; + } + } + + toSql(): string { + return 'NUMBER(19, 0)'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_NUMBER }; + } +} + +export class FLOAT extends BaseTypes.FLOAT { + toSql() { + return 'BINARY_FLOAT'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_BINARY_FLOAT }; + } +} + +export class BLOB extends BaseTypes.BLOB { + toSql(): string { + return 'BLOB'; + } + // check for hexify + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_BLOB }; + } +} + +export class JSON extends BaseTypes.JSON { + toSql(): string { + return 'BLOB'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_BLOB }; + } + + toBindableValue(value: any): string { + if (value === null) { + const sequelize = this._getDialect().sequelize; + + const isExplicit = sequelize.options.nullJsonStringification === 'explicit'; + if (isExplicit) { + throw new Error( + `Attempted to insert the JavaScript null into a JSON column, but the "nullJsonStringification" option is set to "explicit", so Sequelize cannot decide whether to use the SQL NULL or the JSON 'null'. Use the SQL_NULL or JSON_NULL variable instead, or set the option to a different value. See https://sequelize.org/docs/v7/querying/json/ for details.`, + ); + } + } + + return typeof value === 'string' ? value : globalThis.JSON.stringify(value); + } + + getBindParamSql(value: any, options: BindParamOptions): any { + return options.bindParam(Buffer.from(globalThis.JSON.stringify(value))); + } +} + +export class DOUBLE extends BaseTypes.DOUBLE { + protected getNumberSqlTypeName(): string { + return 'DOUBLE PRECISION'; + } + + protected _checkOptionSupport(dialect: AbstractDialect): void { + super._checkOptionSupport(dialect); + + if (this.options.zerofill) { + dialect.warnDataTypeIssue( + `${dialect.name}: ${this.getDataTypeId} doesn't support zerofill option.`, + ); + } + } + + toSql(): string { + return 'BINARY_DOUBLE'; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_BINARY_DOUBLE }; + } +} + +export class DATEONLY extends BaseTypes.DATEONLY { + toBindableValue(date: AcceptedDate) { + if (date) { + const format = 'YYYY/MM/DD'; + + return this.escape(`TO_DATE('${date}','${format}')`); + } + + return this.escape(date); + } + + parseDatabaseValue(value: any) { + if (value) { + return dayjs.utc(value).format('YYYY-MM-DD'); + } + + return value; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_DATE }; + } + + /** + * avoids appending TO_DATE in toBindableValue() + * + * @override + */ + getBindParamSql(value: AcceptedDate, options: BindParamOptions): string { + if (typeof value === 'string') { + return options.bindParam(new Date(value)); + } + + return options.bindParam(value); + } +} diff --git a/packages/oracle/src/connection-manager.ts b/packages/oracle/src/connection-manager.ts new file mode 100644 index 000000000000..a6a3afb3348c --- /dev/null +++ b/packages/oracle/src/connection-manager.ts @@ -0,0 +1,172 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +import type { AbstractConnection, ConnectionOptions } from '@sequelize/core'; +import { + AbstractConnectionManager, + AccessDeniedError, + ConnectionError, + ConnectionRefusedError, + ConnectionTimedOutError, + HostNotReachableError, + InvalidConnectionError, +} from '@sequelize/core'; +import { logger } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/logger.js'; +import type { Connection as oracledbConnection } from 'oracledb'; +import oracledb from 'oracledb'; +import type { OracleDialect } from './dialect.js'; + +export type oracledbModule = typeof oracledb; + +const debug = logger.debugContext('connection:oracle'); + +export interface OracleConnection extends oracledbConnection, AbstractConnection { + on(event: 'error', listener: (err: any) => void): this; +} + +export interface OracleConnectionOptions + extends Omit { + database?: string; + + host?: string; + + port?: number | string; +} + +export class OracleConnectionManager extends AbstractConnectionManager< + OracleDialect, + OracleConnection +> { + readonly #lib: typeof oracledb; + constructor(dialect: OracleDialect) { + super(dialect); + this.extendLib(); + this.#lib = oracledb; + } + + buildConnectString(config: ConnectionOptions) { + if (config.connectString) { + if (config.host || config.database || config.port) { + throw new Error( + 'connectString and host/database/port cannot be accepted simutaneously. Use only connectString instead.', + ); + } + + return config.connectString; + } + + if (!config.host || config.host.length === 0) { + return config.database; + } + + let connectString = config.host; + if (config.port) { + connectString += `:${config.port}`; + } else { + connectString += ':1521'; + } + + if (config.database && config.database.length > 0) { + connectString += `/${config.database}`; + } + + return connectString; + } + + /** + * Method for initializing the lib + */ + extendLib() { + oracledb.fetchAsString = [oracledb.CLOB]; + + // Retrieve BLOB always as Buffer. + oracledb.fetchAsBuffer = [oracledb.BLOB]; + } + + async connect(config: ConnectionOptions): Promise { + const connectionConfig: OracleConnectionOptions = { + connectString: this.buildConnectString(config), + ...config, + }; + + try { + const connection: OracleConnection = (await this.#lib.getConnection( + connectionConfig, + )) as OracleConnection; + + debug('connection acquired'); + connection.on('error', error => { + switch (error.code) { + case 'ESOCKET': + case 'ECONNRESET': + case 'EPIPE': + case 'PROTOCOL_CONNECTION_LOST': + void this.sequelize.pool.destroy(connection); + break; + default: + } + }); + + return connection; + } catch (error: any) { + let errorCode = error.message.split(':'); + errorCode = errorCode[0]; + + switch (errorCode) { + 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 + case 'NJS-511': // NJS-511: connection refused + case 'NJS-516': // NJS-516: No Config Dir + case 'NJS-517': // NJS-517: TNS Entry not found + case 'NJS-520': // NJS-520: TNS Names File missing + throw new ConnectionRefusedError(error); + 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 + case 'NJS-506': // NJS-506: TLS Auth Failure + throw new AccessDeniedError(error); + case 'ORA-12541': // ORA-12541: TNS: No listener + case 'NJS-503': // NJS-503: Connection Incomplete + case 'NJS-508': // NJS-508: TLS HOST MATCH Failure + case 'NJS-507': // NJS-507: TLS DN MATCH Failure + throw new HostNotReachableError(error); + case 'NJS-512': // NJS-512: Invalid Connect String Parameters + case 'NJS-515': // NJS-515: Invalid EZCONNECT Syntax + case 'NJS-518': // NJS-518: Invald ServiceName + case 'NJS-519': // NJS-519: Invald SID + throw new InvalidConnectionError(error); + case 'ORA-12170': // ORA-12170: TNS: Connect Timeout occurred + case 'NJS-510': // NJS-510: Connect Timeout occurred + throw new ConnectionTimedOutError(error); + default: + throw new ConnectionError(error); + } + } + } + + async disconnect(connection: OracleConnection) { + if (!connection.isHealthy()) { + debug('connection tried to disconnect but was already at CLOSED state'); + + return; + } + + await new Promise((resolve, reject) => { + connection.close(error => { + if (error) { + // eslint-disable-next-line prefer-promise-reject-errors + return void reject(); + } + + resolve(); + + return undefined; + }); + }); + } + + validate(connection: OracleConnection): boolean { + return connection && connection.isHealthy(); + } +} diff --git a/packages/oracle/src/dialect.ts b/packages/oracle/src/dialect.ts new file mode 100644 index 000000000000..f43191c8ba72 --- /dev/null +++ b/packages/oracle/src/dialect.ts @@ -0,0 +1,190 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +import type { Sequelize } from '@sequelize/core'; +import { AbstractDialect } from '@sequelize/core'; +import type { SupportableNumericOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/dialect.js'; +import { createNamedParamBindCollector } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/sql.js'; +import { EMPTY_ARRAY } from '@sequelize/utils'; +import { CONNECTION_OPTION_NAMES } from './_internal/connection-options.js'; +import * as DataTypes from './_internal/data-types-overrides'; +import { OracleConnectionManager } from './connection-manager'; +import type { OracleConnectionOptions } from './connection-manager.js'; +import { OracleQueryGenerator } from './query-generator.js'; +import { OracleQueryInterface } from './query-interface.js'; +import { OracleQuery } from './query.js'; + +export interface OracleDialectOptions {} + +const numericOptions: SupportableNumericOptions = { + zerofill: false, + unsigned: true, +}; + +export class OracleDialect extends AbstractDialect { + static readonly supports = AbstractDialect.extendSupport({ + 'VALUES ()': true, + 'LIMIT ON UPDATE': true, + lock: false, + forShare: 'LOCK IN SHARE MODE', + index: { + collate: false, + length: false, + parser: false, + type: false, + using: false, + }, + constraints: { + restrict: false, + onUpdate: false, + }, + returnValues: false, + returnIntoValues: true, + 'ORDER NULLS': true, + schemas: true, + inserts: { + updateOnDuplicate: false, + }, + indexViaAlter: false, + dataTypes: { + COLLATE_BINARY: true, + GEOMETRY: false, + JSON: true, + INTS: numericOptions, + DOUBLE: numericOptions, + DECIMAL: { unconstrained: true }, + TIME: { + precision: false, + }, + }, + jsonOperations: true, + jsonExtraction: { + quoted: true, + }, + dropTable: { + cascade: true, + }, + renameTable: { + changeSchema: false, + }, + delete: { + limit: true, + }, + startTransaction: { + useBegin: true, + }, + upserts: true, + bulkDefault: true, + topLevelOrderByRequired: true, + }); + + readonly connectionManager: OracleConnectionManager; + readonly queryGenerator: OracleQueryGenerator; + readonly queryInterface: OracleQueryInterface; + readonly Query = OracleQuery; + + constructor(sequelize: Sequelize, options: OracleDialectOptions) { + super({ + dataTypesDocumentationUrl: + 'https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html', + minimumDatabaseVersion: '18.0.0', + identifierDelimiter: '"', + name: 'oracle', + options, + sequelize, + dataTypeOverrides: DataTypes, + }); + + this.connectionManager = new OracleConnectionManager(this); + this.queryGenerator = new OracleQueryGenerator(this); + this.queryInterface = new OracleQueryInterface(this); + } + + parseConnectionUrl(): OracleConnectionOptions { + throw new Error( + 'The "url" option is not supported by the Oracle dialect. Instead, please use the "connectionString" option.', + ); + } + + getDefaultSchema(): string { + return this.sequelize.options.replication.write.username?.toUpperCase() ?? ''; + } + + createBindCollector() { + return createNamedParamBindCollector(':'); + } + + escapeString(val: string): string { + if (val.startsWith('TO_TIMESTAMP_TZ') || val.startsWith('TO_DATE')) { + this.assertDate(val); + + return val; + } + + val = val.replaceAll("'", "''"); + + return `'${val}'`; + } + + escapeBuffer(buffer: Buffer): string { + const hex = buffer.toString('hex'); + + return `'${hex}'`; + } + + // assert date to avoid SQL injections. + assertDate(val: string): void { + // Split the string using parentheses to isolate the function name, parameters, and potential extra parts + const splitVal = val.split(/\(|\)/); + + // Validate that the split result has exactly three parts (function name, parameters, and an empty string) + // and that there are no additional SQL commands after the function call (indicated y the last empty string). + if (splitVal.length !== 3 || splitVal[2] !== '') { + throw new Error('Invalid SQL function call.'); // Error if function call has unexpected format + } + + // Extract the function name (etiher 'TO_TIMESTAM_TZ' or 'TO_DATE') and the contents inside the parantheses + const functionName = splitVal[0].trim(); // Function name should be 'TO_TIMESTAMP_TZ' or 'TO_DATE' + const insideParens = splitVal[1].trim(); // This contains the parameters (date value and format string) + + if (!['TO_TIMESTAMP_TZ', 'TO_DATE'].includes(functionName)) { + throw new Error('Invalid SQL function call. Expected TO_TIMESTAMP_TZ or TO_DATE.'); + } + + // Split the parameters inside the parentheses by commas (should contain exactly two: date and format) + const params = insideParens.split(','); + + // Validate that the parameters contain exactly two parts (date value and format string) + if (params.length !== 2) { + throw new Error( + 'Unexpected input received.\nSequelize supports TO_TIMESTAMP_TZ or TO_DATE exclusively with a combination of value and format.', + ); + } + + // Extract the format value (second parameter) and remove single quotes around it + const formatValue = params[1].trim(); + + if (functionName === 'TO_TIMESTAMP_TZ') { + const expectedFormat = "'YYYY-MM-DD HH24:MI:SS.FFTZH:TZM'"; + // Validate that the formatValue is equal to expectedFormat since that is the only format used within sequelize + if (formatValue !== expectedFormat) { + throw new Error( + `Invalid format string for TO_TIMESTAMP_TZ. Expected format: ${expectedFormat}`, + ); + } + } else if (functionName === 'TO_DATE') { + const expectedFormat = "'YYYY/MM/DD'"; + // Validate that the formatValue is equal to expectedFormat since that is the only format used within sequelize + if (formatValue !== expectedFormat) { + throw new Error(`Invalid format string for TO_DATE. Expected format: ${expectedFormat}`); + } + } + } + + static getSupportedOptions() { + return EMPTY_ARRAY; + } + + static getSupportedConnectionOptions() { + return CONNECTION_OPTION_NAMES; + } +} diff --git a/packages/oracle/src/index.mjs b/packages/oracle/src/index.mjs new file mode 100644 index 000000000000..93a13199ae04 --- /dev/null +++ b/packages/oracle/src/index.mjs @@ -0,0 +1,7 @@ +import Pkg from './index.js'; + +export const OracleConnectionManager = Pkg.OracleConnectionManager; +export const OracleDialect = Pkg.OracleDialect; +export const OracleQueryGenerator = Pkg.OracleQueryGenerator; +export const OracleQueryInterface = Pkg.OracleQueryInterface; +export const OracleQuery = Pkg.OracleQuery; diff --git a/packages/oracle/src/index.ts b/packages/oracle/src/index.ts new file mode 100644 index 000000000000..77a8f97c40cc --- /dev/null +++ b/packages/oracle/src/index.ts @@ -0,0 +1,7 @@ +/** Generated File, do not modify directly. Run "yarn sync-exports" in the folder of the package instead */ + +export * from './connection-manager.js'; +export * from './dialect.js'; +export * from './query-generator.js'; +export * from './query-interface.js'; +export * from './query.js'; diff --git a/packages/oracle/src/query-generator-typescript.internal.ts b/packages/oracle/src/query-generator-typescript.internal.ts new file mode 100644 index 000000000000..fcdd2f0bdb92 --- /dev/null +++ b/packages/oracle/src/query-generator-typescript.internal.ts @@ -0,0 +1,332 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +import type { + BulkDeleteQueryOptions, + CreateSchemaQueryOptions, + RemoveColumnQueryOptions, + RemoveConstraintQueryOptions, + RemoveIndexQueryOptions, + RenameTableQueryOptions, + TableOrModel, + TruncateTableQueryOptions, +} from '@sequelize/core'; +import { AbstractQueryGenerator, IsolationLevel } from '@sequelize/core'; +import { + CREATE_SCHEMA_QUERY_SUPPORTABLE_OPTIONS, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTIONS, + REMOVE_INDEX_QUERY_SUPPORTABLE_OPTIONS, + RENAME_TABLE_QUERY_SUPPORTABLE_OPTIONS, + TRUNCATE_TABLE_QUERY_SUPPORTABLE_OPTIONS, +} from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-typescript.js'; +import type { TableNameWithSchema } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-interface.js'; +import { rejectInvalidOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/check.js'; +import { joinSQLFragments } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/join-sql-fragments.js'; +import { + extractModelDefinition, + isModelStatic, +} from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/model-utils.js'; +import { EMPTY_SET } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/object.js'; +import { generateIndexName } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/string.js'; +import type { OracleDialect } from './dialect.js'; +import { OracleQueryGeneratorInternal } from './query-generator.internal.js'; + +export class OracleQueryGeneratorTypeScript extends AbstractQueryGenerator { + readonly #internals: OracleQueryGeneratorInternal; + + constructor( + dialect: OracleDialect, + internals: OracleQueryGeneratorInternal = new OracleQueryGeneratorInternal(dialect), + ) { + super(dialect, internals); + + this.#internals = internals; + } + + describeTableQuery(tableName: TableOrModel) { + const table = this.extractTableDetails(tableName); + const currTableName = this.getCatalogName(table.tableName); + const schema = this.getCatalogName(table.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, ucc.constraint_type ', + 'FROM all_tab_columns atc ', + '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 atc.COLUMN_NAME, CONSTRAINT_TYPE DESC', + ].join(''); + } + + removeIndexQuery( + tableName: TableOrModel, + indexNameOrAttributes: string | string[], + options: RemoveIndexQueryOptions, + ) { + if (options) { + rejectInvalidOptions( + 'removeIndexQuery', + this.dialect, + REMOVE_INDEX_QUERY_SUPPORTABLE_OPTIONS, + EMPTY_SET, + options, + ); + } + + let indexName: string; + if (Array.isArray(indexNameOrAttributes)) { + const table = this.extractTableDetails(tableName); + indexName = generateIndexName(table, { fields: indexNameOrAttributes }); + } else { + indexName = indexNameOrAttributes; + } + + return `DROP INDEX ${this.quoteIdentifier(indexName)}`; + } + + /** + * Returns the value as it is stored in the Oracle DB + * + * @param value + */ + getCatalogName(value: string | undefined) { + if (value && this.options.quoteIdentifiers === false) { + const quotedValue = this.quoteIdentifier(value); + if (quotedValue === value) { + value = value.toUpperCase(); + } + } + + return value; + } + + showIndexesQuery(table: TableNameWithSchema) { + const [tableName, owner] = this.getSchemaNameAndTableName(table); + const sql = [ + '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', + ' ORDER BY index_name, column_position', + ]; + + return sql.join(''); + } + + /** + * Returns the tableName and schemaName as it is stored the Oracle DB + * + * @param table + */ + getSchemaNameAndTableName(table: any) { + table = this.extractTableDetails(table); + const tableName = this.getCatalogName(table.tableName || table); + const schemaName = this.getCatalogName(table.schema); + + return [tableName, schemaName]; + } + + removeConstraintQuery( + tableName: TableOrModel, + constraintName: string, + options?: RemoveConstraintQueryOptions, + ) { + if (constraintName.startsWith('sys')) { + return joinSQLFragments([ + 'ALTER TABLE', + this.quoteTable(tableName), + 'DROP CONSTRAINT', + options?.ifExists ? 'IF EXISTS' : '', + constraintName, + options?.cascade ? 'CASCADE' : '', + ]); + } + + return super.removeConstraintQuery(tableName, constraintName, options); + } + + renameTableQuery( + beforeTableName: TableOrModel, + afterTableName: TableOrModel, + options?: RenameTableQueryOptions, + ): string { + if (options) { + rejectInvalidOptions( + 'renameTableQuery', + this.dialect, + RENAME_TABLE_QUERY_SUPPORTABLE_OPTIONS, + EMPTY_SET, + options, + ); + } + + const beforeTable = this.extractTableDetails(beforeTableName); + const afterTable = this.extractTableDetails(afterTableName); + const renamedTable = afterTable.tableName; + + if (beforeTable.schema !== afterTable.schema) { + throw new Error( + `Moving tables between schemas is not supported by ${this.dialect.name} dialect.`, + ); + } + + return `ALTER TABLE ${this.quoteTable(beforeTableName)} RENAME TO ${this.quoteTable(renamedTable)}`; + } + + removeColumnQuery( + tableName: TableOrModel, + attributeName: string, + options: RemoveColumnQueryOptions, + ): string { + rejectInvalidOptions( + 'removeColumnQuery', + this.dialect, + REMOVE_COLUMN_QUERY_SUPPORTABLE_OPTIONS, + EMPTY_SET, + options, + ); + + return joinSQLFragments([ + 'ALTER TABLE', + this.quoteTable(tableName), + 'DROP COLUMN', + this.quoteIdentifier(attributeName), + ]); + } + + createSchemaQuery(schema: string, options: CreateSchemaQueryOptions): string { + if (options) { + rejectInvalidOptions( + 'createSchemaQuery', + this.dialect, + CREATE_SCHEMA_QUERY_SUPPORTABLE_OPTIONS, + EMPTY_SET, + options, + ); + } + + const quotedSchema = this.quoteIdentifier(schema); + + return [ + 'DECLARE', + 'USER_FOUND BOOLEAN := FALSE;', + 'BEGIN', + ' BEGIN', + ' EXECUTE IMMEDIATE ', + this.escape(`CREATE USER ${quotedSchema} IDENTIFIED BY 12345 DEFAULT TABLESPACE USERS`), + ';', + ' EXCEPTION WHEN OTHERS THEN', + ' IF SQLCODE != -1920 THEN', + ' RAISE;', + ' ELSE', + ' USER_FOUND := TRUE;', + ' END IF;', + ' END;', + ' IF NOT USER_FOUND THEN', + ' EXECUTE IMMEDIATE ', + this.escape(`GRANT "CONNECT" TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + this.escape(`GRANT CREATE TABLE TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + this.escape(`GRANT CREATE VIEW TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + this.escape(`GRANT CREATE ANY TRIGGER TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + this.escape(`GRANT CREATE ANY PROCEDURE TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + this.escape(`GRANT CREATE SEQUENCE TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + this.escape(`GRANT CREATE SYNONYM TO ${quotedSchema}`), + ';', + ' EXECUTE IMMEDIATE ', + this.escape(`ALTER USER ${quotedSchema} QUOTA UNLIMITED ON USERS`), + ';', + ' END IF;', + 'END;', + ].join(' '); + } + + truncateTableQuery(tableName: TableOrModel, options: TruncateTableQueryOptions): string { + if (options) { + rejectInvalidOptions( + 'truncateTableQuery', + this.dialect, + TRUNCATE_TABLE_QUERY_SUPPORTABLE_OPTIONS, + EMPTY_SET, + options, + ); + } + + return `TRUNCATE TABLE ${this.quoteTable(tableName)}`; + } + + bulkDeleteQuery(tableName: TableOrModel, options: BulkDeleteQueryOptions): string { + const table = this.quoteTable(tableName); + const modelDefinition = extractModelDefinition(tableName); + const whereOptions = isModelStatic(tableName) ? { ...options, model: tableName } : options; + let queryTmpl; + + let whereClause = this.whereQuery(options.where, whereOptions); + whereClause = whereClause.replace('WHERE', ''); + + if (options.limit && this.dialect.supports.delete.limit) { + if (!modelDefinition) { + throw new Error( + 'Using LIMIT in bulkDeleteQuery requires specifying a model or model definition.', + ); + } + + const whereTmpl = whereClause ? ` AND ${whereClause}` : ''; + queryTmpl = `DELETE FROM ${table} WHERE rowid IN (SELECT rowid FROM ${table} WHERE rownum <= ${this.escape(options.limit)}${whereTmpl})`; + } else { + const whereTmpl = whereClause ? ` WHERE${whereClause}` : ''; + queryTmpl = `DELETE FROM ${table}${whereTmpl}`; + } + + return queryTmpl; + } + + setIsolationLevelQuery(isolationLevel: IsolationLevel): string { + switch (isolationLevel) { + case IsolationLevel.READ_UNCOMMITTED: + case IsolationLevel.READ_COMMITTED: + return 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED'; + case IsolationLevel.REPEATABLE_READ: + case IsolationLevel.SERIALIZABLE: + // Serializable mode is equal to Snapshot Isolation (SI) + // defined in ANSI std. + return 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE'; + default: + throw new Error( + `The ${isolationLevel} isolation level is not supported by ${this.dialect.name}.`, + ); + } + } + + commitTransactionQuery() { + return 'COMMIT TRANSACTION'; + } + + rollbackTransactionQuery(): string { + if (this.dialect.supports.connectionTransactionMethods) { + throw new Error( + `rollbackTransactionQuery is not supported by the ${this.dialect.name} dialect.`, + ); + } + + return 'ROLLBACK TRANSACTION'; + } +} diff --git a/packages/oracle/src/query-generator.d.ts b/packages/oracle/src/query-generator.d.ts new file mode 100644 index 000000000000..3272395c6757 --- /dev/null +++ b/packages/oracle/src/query-generator.d.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +import { OracleQueryGeneratorTypeScript } from './query-generator-typescript.internal.ts'; + +export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript {} diff --git a/packages/oracle/src/query-generator.internal.ts b/packages/oracle/src/query-generator.internal.ts new file mode 100644 index 000000000000..75b479f5a740 --- /dev/null +++ b/packages/oracle/src/query-generator.internal.ts @@ -0,0 +1,54 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +import { attributeTypeToSql } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types-utils.js'; +import { AbstractQueryGeneratorInternal } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-internal.js'; +import type { EscapeOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-typescript.js'; +import type { AddLimitOffsetOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator.internal-types.js'; +import { wrapAmbiguousWhere } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/where-sql-builder.js'; +import type { Cast } from '@sequelize/core/_non-semver-use-at-your-own-risk_/expression-builders/cast.js'; +import type { OracleDialect } from './dialect.js'; + +export class OracleQueryGeneratorInternal< + Dialect extends OracleDialect = OracleDialect, +> extends AbstractQueryGeneratorInternal { + addLimitAndOffset(options: AddLimitOffsetOptions) { + let fragment = ''; + const offset = options.offset || 0; + + if (options.offset || options.limit) { + fragment += ` OFFSET ${this.queryGenerator.escape(offset, options)} ROWS`; + } + + if (options.limit) { + fragment += ` FETCH NEXT ${this.queryGenerator.escape(options.limit, options)} ROWS ONLY`; + } + + return fragment; + } + + formatCast(cast: Cast, options?: EscapeOptions | undefined): string { + const type = this.sequelize.normalizeDataType(cast.type); + + let castSql = wrapAmbiguousWhere( + cast.expression, + this.queryGenerator.escape(cast.expression, { ...options, type }), + ); + const targetSql = attributeTypeToSql(type).toUpperCase(); + + if (type === 'boolean') { + castSql = `(CASE WHEN ${castSql}='true' THEN 1 ELSE 0 END)`; + + return `CAST(${castSql} AS NUMBER)`; + } else if (type === 'TIMESTAMPTZ') { + castSql = castSql.slice(0, -1); + + return `${castSql} RETURNING TIMESTAMP WITH TIME ZONE)`; + } + + return `CAST(${castSql} AS ${targetSql})`; + } + + getAliasToken(): string { + return ''; + } +} diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js new file mode 100644 index 000000000000..fe517c0f4edb --- /dev/null +++ b/packages/oracle/src/query-generator.js @@ -0,0 +1,1294 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +'use strict'; + +import each from 'lodash/each'; +import forOwn from 'lodash/forOwn'; +import includes from 'lodash/includes'; +import isPlainObject from 'lodash/isPlainObject'; +import toPath from 'lodash/toPath'; +import oracledb from 'oracledb'; + +import { DataTypes } from '@sequelize/core'; +import { normalizeDataType } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types-utils.js'; +import { + ADD_COLUMN_QUERY_SUPPORTABLE_OPTIONS, + CREATE_TABLE_QUERY_SUPPORTABLE_OPTIONS, +} from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator.js'; +import { rejectInvalidOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/check.js'; +import { quoteIdentifier } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/dialect.js'; +import { joinSQLFragments } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/join-sql-fragments.js'; +import { + EMPTY_OBJECT, + EMPTY_SET, + getObjectFromMap, +} from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/object.js'; +import { defaultValueSchemable } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/query-builder-utils.js'; +import { OracleQueryGeneratorTypeScript } from './query-generator-typescript.internal'; + +const CREATE_TABLE_QUERY_SUPPORTED_OPTIONS = new Set(['uniqueKeys']); + +/** + * list of reserved words in Oracle DB 21c + * source: https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=GUID-7B72E154-677A-4342-A1EA-C74C1EA928E6 + * + * @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; + +export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { + listSchemasQuery() { + return 'SELECT USERNAME AS "schema" FROM ALL_USERS WHERE COMMON = (\'NO\') AND USERNAME != user'; + } + + dropSchemaQuery(schema) { + return [ + 'BEGIN', + 'EXECUTE IMMEDIATE ', + this.escape(`DROP USER ${this.quoteTable(schema)} CASCADE`), + ';', + 'EXCEPTION WHEN OTHERS THEN', + ' IF SQLCODE != -1918 THEN', + ' RAISE;', + ' END IF;', + 'END;', + ].join(' '); + } + + versionQuery() { + return `SELECT VERSION_FULL FROM PRODUCT_COMPONENT_VERSION WHERE PRODUCT LIKE 'Oracle%'`; + } + + createTableQuery(tableName, attributes, options) { + if (options) { + rejectInvalidOptions( + 'createTableQuery', + this.dialect, + CREATE_TABLE_QUERY_SUPPORTABLE_OPTIONS, + CREATE_TABLE_QUERY_SUPPORTED_OPTIONS, + options, + ); + } + + const primaryKeys = []; + const foreignKeys = Object.create(null); + const attrStr = []; + const checkStr = []; + + const values = { + table: this.quoteTable(tableName), + }; + + // Starting by dealing with all attributes + for (let attr in attributes) { + if (!Object.hasOwn(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 { + 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.join(', '); + + if (pkString.length > 0) { + values.attributes += `,PRIMARY KEY (${pkString})`; + } + + // Dealing with FKs + for (const fkey in foreignKeys) { + if (!Object.hasOwn(foreignKeys, fkey)) { + continue; + } + + // Oracle default response for FK, doesn't support if defined + if (foreignKeys[fkey].includes('ON DELETE NO ACTION')) { + foreignKeys[fkey] = foreignKeys[fkey].replace('ON DELETE NO ACTION', ''); + } + + values.attributes += `,FOREIGN KEY (${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); + + // eslint-disable-next-line unicorn/no-for-loop + for (let fieldIdx = 0; fieldIdx < keys.length; fieldIdx++) { + const currUnique = options.uniqueKeys[keys[fieldIdx]]; + + if (currUnique.fields.length === fields.length) { + let i; + // lengths are the same, possible same constraint + for (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 (i === currUnique.fields.length) { + break; + } + } + } + + if (canContinue) { + const indexName = 'name' in index ? index.name : ''; + 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?.uniqueKeys) { + // only need to sort primary keys once, don't do it in place + let sortedPrimaryKeys = [...primaryKeys]; + sortedPrimaryKeys = sortedPrimaryKeys.map(elem => { + return elem.replaceAll('"', ''); + }); + sortedPrimaryKeys.sort(); + each(options.uniqueKeys, (columns, indexName) => { + const sortedColumnFields = [...columns.fields]; + sortedColumnFields.sort(); + // if primary keys === unique keys, then skip adding new constraint + const uniqueIsPrimary = + sortedColumnFields.length === primaryKeys.length && + sortedColumnFields.every((value, index) => { + return value === sortedPrimaryKeys[index]; + }); + if (uniqueIsPrimary) { + return true; + } + + // generate Constraint name, if no indexName is given + if (typeof indexName !== 'string') { + indexName = `uniq_${tableName}_${columns.fields.join('_')}`; + } + + values.attributes += `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`; + }); + } + + // we replace single quotes by two quotes in order for the execute statement to work + const query = joinSQLFragments(['CREATE TABLE', values.table, `(${values.attributes})`]); + + return joinSQLFragments([ + 'BEGIN', + 'EXECUTE IMMEDIATE', + `${this.escape(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 = ${this.escape(tableName)} AND OWNER = ${table.schema ? this.escape(schemaName) : 'USER'}`; + } + + showConstraintsQuery(tableName, options) { + if (options && options.constraintType === 'FOREIGN KEY') { + return this.getForeignKeysQuery(tableName); + } + + let table = this.extractTableDetails(tableName); + const schema = this.getCatalogName(table.schema); + table = this.getCatalogName(table.tableName); + + return joinSQLFragments([ + 'SELECT C.CONSTRAINT_NAME "constraintName",', + `CASE A.CONSTRAINT_TYPE WHEN 'P' THEN 'PRIMARY KEY' WHEN 'R' THEN 'FOREIGN KEY' WHEN 'C' THEN 'CHECK' WHEN 'U' THEN 'UNIQUE' ELSE NULL END "constraintType",`, + 'C.TABLE_NAME "tableName",', + 'C.OWNER "constraintSchema",', + 'C.COLUMN_NAME "columnNames"', + 'FROM ALL_CONS_COLUMNS C', + 'INNER JOIN ALL_CONSTRAINTS A ON C.CONSTRAINT_NAME = A.CONSTRAINT_NAME', + `WHERE C.TABLE_NAME =${this.escape(table)}`, + `AND C.OWNER =${this.escape(schema)}`, + options?.constraintName + ? `AND C.CONSTRAINT_NAME =${this.escape(options.constraintName)}` + : '', + options?.constraintType + ? `AND A.CONSTRAINT_TYPE =${this.escape(this._getConstraintType(options.constraintType))}` + : '', + 'ORDER BY C.CONSTRAINT_NAME', + ]); + } + + _getConstraintType(type) { + switch (type) { + case 'CHECK': + return 'C'; + case 'FOREIGN KEY': + return 'R'; + case 'PRIMARY KEY': + return 'P'; + case 'UNIQUE': + return 'U'; + default: + throw new Error(`Constraint type ${type} is not supported`); + } + } + + listTablesQuery(options) { + let query = `SELECT owner as "schema", table_name as "tableName" FROM all_tables where OWNER IN`; + if (options && options.schema) { + query += `(SELECT USERNAME AS "schema_name" FROM ALL_USERS WHERE ORACLE_MAINTAINED = 'N' AND USERNAME=${this.escape(options.schema)})`; + } else { + query += `(SELECT USERNAME AS "schema_name" FROM ALL_USERS WHERE ORACLE_MAINTAINED = 'N')`; + } + + return query; + } + + dropTableQuery(tableName) { + return 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); + + // tableName = this.quoteTable(tableName); + // return `ALTER TABLE ${tableName} ADD ${constraintSnippet};`; + // } + + addColumnQuery(table, key, dataType, options) { + if (options) { + rejectInvalidOptions( + 'addColumnQuery', + this.dialect, + ADD_COLUMN_QUERY_SUPPORTABLE_OPTIONS, + EMPTY_SET, + options, + ); + } + + dataType = { + ...dataType, + field: key, + type: normalizeDataType(dataType.type, this.dialect), + }; + dataType.field = key; + + const attribute = joinSQLFragments([ + this.quoteIdentifier(key), + this.attributeToSQL(dataType, { + attributeName: key, + context: 'addColumn', + }), + ]); + + return joinSQLFragments(['ALTER TABLE', this.quoteTable(table), 'ADD', attribute, ';']); + } + + /** + * 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 {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(definition, table, attributeName) { + const [tableName, schemaName] = this.getSchemaNameAndTableName(table); + 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', + ')', + 'WHERE owner =', + schemaNameConstant, + 'AND table_name =', + tableNameConstant, + 'AND cons_columns =', + attributeNameConstant, + ';', + ].join(' '); + const secondQuery = joinSQLFragments([ + `ALTER TABLE ${this.quoteTable(table)}`, + 'ADD FOREIGN KEY', + `(${this.quoteIdentifier(attributeName)})`, + definition.replace(/.+?(?=REFERENCES)/, ''), + ]); + + return [ + '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 ${this.escape(secondQuery)};`, + ].join(' '); + } + + /** + * Function to alter table modify + * + * @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(definition, table, attributeName) { + const query = joinSQLFragments([ + 'ALTER TABLE', + this.quoteTable(table), + 'MODIFY', + this.quoteIdentifier(attributeName), + definition, + ]); + const secondQuery = query.replace('NOT NULL', '').replace('NULL', ''); + + return [ + 'BEGIN', + `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 ${this.escape(secondQuery)};`, + ' ELSE', + ' RAISE;', + ' END IF;', + 'END;', + ].join(' '); + } + + changeColumnQuery(table, attributes) { + const sql = ['DECLARE', 'CONS_NAME VARCHAR2(200);', 'BEGIN']; + for (const attributeName in attributes) { + if (!Object.hasOwn(attributes, attributeName)) { + continue; + } + + const definition = attributes[attributeName]; + // eslint-disable-next-line unicorn/prefer-regexp-test + if (definition.match(/REFERENCES/)) { + sql.push(this._alterForeignKeyConstraint(definition, table, attributeName)); + } else { + // Building the modify query + sql.push(this._modifyQuery(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 options.outBindAttributes map with bindDef for outbind of InsertQuery + * + * @param {Array} returningModelAttributes + * @param {Array} returnTypes + * @param {number} inbindLength + * @param {object} returnAttributes + * @param {object} options + * + * @private + */ + populateInsertQueryReturnIntoBinds( + returningModelAttributes, + returnTypes, + inbindLength, + returnAttributes, + options, + ) { + 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 (element.startsWith('"')) { + element = element.slice(1, -1); + } + + outBindAttributes[element] = Object.assign(returnTypes[index]._getBindDef(oracledb), { + dir: oracledb.BIND_OUT, + }); + const returnAttribute = `${outbindParam(undefined)}`; + 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 modelDefinition = model.modelDefinition; + const rawAttributes = getObjectFromMap(modelDefinition.attributes); + 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(''); + + if (options.bindParam !== false) { + options.bind = updateQuery.bind || insertQuery.bind; + } + + return query; + } + + /** + * 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 = {}; + + // 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], + let tuple = []; + const bindMap = {}; + // 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(bindMap) : 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 new 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.escape(fieldValueHash[key] ?? null, { + model: options.model, + type: fieldMappedAttributes[key] ? fieldMappedAttributes[key].type : null, + bindParam: 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; + } + + tuple = Object.values(bindMap); + // 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 = 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 = 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 + options.bind = tuples; + // Setting options.inbindAttribute + options.inbindAttributes = inBindBindDefMap; + + return result; + } + + deleteQuery(tableName, where, options = EMPTY_OBJECT, model) { + const table = tableName; + + let whereClause = this.whereQuery(where, { ...options, model }); + whereClause = whereClause.replace('WHERE', ''); + 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 = whereClause ? ` AND ${whereClause}` : ''; + queryTmpl = `DELETE FROM ${this.quoteTable(table)} WHERE rowid IN (SELECT rowid FROM ${this.quoteTable(table)} WHERE rownum <= ${this.escape(options.limit)}${whereTmpl})`; + } else { + const whereTmpl = whereClause ? ` WHERE${whereClause}` : ''; + queryTmpl = `DELETE FROM ${this.quoteTable(table)}${whereTmpl}`; + } + + return queryTmpl; + } + + attributeToSQL(attribute, options) { + if (!isPlainObject(attribute)) { + attribute = { + type: attribute, + }; + } + + // handle self referential constraints + if ( + attribute.references && + attribute.Model && + attribute.Model.tableName === attribute.references.tableName + ) { + 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) { + // enums are a special case + template = attribute.type.toSql({ dialect: this.dialect }); + template += ` CHECK (${this.quoteIdentifier(options.attributeName)} IN(${attribute.type.options.values + .map(value => { + return this.escape(value, undefined, {}); + }) + .join(', ')}))`; + + return template; + } + + if (attribute.type instanceof DataTypes.JSON) { + template = attribute.type.toSql(); + template += ` CHECK (${this.quoteIdentifier(options.attributeName)} IS JSON)`; + + return template; + } + + if (attribute.type instanceof DataTypes.BOOLEAN) { + template = attribute.type.toSql(); + template += ` CHECK (${this.quoteIdentifier(options.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 === 'DOUBLE') { + 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?.options?.unsigned) { + attribute.type.options.unsigned = false; + unsignedTemplate += ` CHECK(${this.quoteIdentifier(options.attributeName)} >= 0)`; + } + + template = attribute.type.toString(); + + // Blobs/texts cannot have a defaultValue + if ( + attribute.type && + attribute.type !== 'TEXT' && + attribute.type._binary !== true && + defaultValueSchemable(attribute.defaultValue, this.dialect) + ) { + template += ` DEFAULT ${this.escape(attribute.defaultValue)}`; + } + + if (!attribute.autoIncrement) { + // If autoincrement, not null is set automatically + if (attribute.allowNull === false) { + template += ' NOT NULL'; + } else if ( + !attribute.primaryKey && + !defaultValueSchemable(attribute.defaultValue, this.dialect) + ) { + template += ' NULL'; + } + } + + template += unsignedTemplate; + } else { + template = ''; + } + + if (attribute.primaryKey) { + template += ' PRIMARY KEY'; + } + + if ((!options || !options.withoutForeignKeyConstraints) && attribute.references) { + template += ` REFERENCES ${this.quoteTable(attribute.references.table)}`; + + 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, { attributeName, ...options }); + } + + 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 = ', + this.escape(tableName), + ' and OWNER = ', + table.schema ? this.escape(schemaName) : 'USER', + ' and COLUMN_NAME = ', + this.escape(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 tableDetails = this.extractTableDetails(table); + const tableName = this.getCatalogName(tableDetails.tableName); + const schemaName = this.getCatalogName(tableDetails.schema); + const sql = [ + 'SELECT DISTINCT a.table_name "tableName", a.constraint_name "constraintName", a.owner "constraintSchema", a.column_name "columnNames",', + `CASE c.CONSTRAINT_TYPE WHEN 'P' THEN 'PRIMARY KEY' WHEN 'R' THEN 'FOREIGN KEY' WHEN 'C' THEN 'CHECK' WHEN 'U' THEN 'UNIQUE' ELSE NULL END "constraintType",`, + ' c.r_owner "referencedTableSchema",', + ' c.DELETE_RULE "deleteAction",', + ' b.table_name "referencedTableName", b.column_name "referencedColumnNames"', + ' 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 = ', + this.escape(tableName), + ' AND a.owner = ', + tableDetails.schema && schemaName !== '' ? this.escape(schemaName) : 'USER', + ' ORDER BY a.table_name, a.column_name, b.column_name', + ].join(''); + + return sql; + } + + 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 = ', + this.escape(tableName), + 'AND cols.owner = ', + table.schema ? this.escape(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; + } + + dropConstraintQuery(tableName, constraintName) { + return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${constraintName}`; + } + + // 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.slice(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; + } + + isIdentifierQuoted(identifier) { + return /^\s*(?:([`"'])(?:(?!\1).|\1{2})*\1\.?)+\s*$/i.test(identifier); + } + + addTicks(identifier, tickChar) { + identifier = identifier.replaceAll(new RegExp(tickChar, 'g'), ''); + + return tickChar + identifier + tickChar; + } + + jsonPathExtractionQuery(column, path) { + let paths = toPath(path); + const quotedColumn = this.isIdentifierQuoted(column) ? column : this.quoteIdentifier(column); + + paths = paths.map(subPath => { + return /\D/.test(subPath) ? this.addTicks(subPath, '"') : subPath; + }); + + const pathStr = this.escape( + ['$'] + .concat(paths) + .join('.') + .replaceAll(/\.(\d+)(?:(?=\.)|$)/g, (__, digit) => `[${digit}]`), + ); + const extractQuery = `json_value(${quotedColumn},${pathStr})`; + + return extractQuery; + } + + booleanValue(value) { + return value ? 1 : 0; + } + + quoteIdentifier(identifier, force = false) { + const optForceQuote = force; + const optQuoteIdentifiers = this.options.quoteIdentifiers !== false; + const regExp = /^(([\w][\w\d_]*))$/g; + + if ( + optForceQuote !== true && + optQuoteIdentifiers === false && + regExp.test(identifier) && + !ORACLE_RESERVED_WORDS.includes(identifier.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 identifier; + } + + return quoteIdentifier(identifier, this.dialect.TICK_CHAR_LEFT, this.dialect.TICK_CHAR_RIGHT); + } + + /** + * 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) { + let i = Object.keys(bind).length; + + return value => { + const bindName = `sequelize_${++i}`; + bind[bindName] = value; + + return `:${Object.keys(bind).length + posOffset}`; + }; + } + + /** + * 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.`); +} diff --git a/packages/oracle/src/query-interface-typescript.internal.ts b/packages/oracle/src/query-interface-typescript.internal.ts new file mode 100644 index 000000000000..912f4df55061 --- /dev/null +++ b/packages/oracle/src/query-interface-typescript.internal.ts @@ -0,0 +1,45 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +import type { FetchDatabaseVersionOptions, QiDropAllTablesOptions } from '@sequelize/core'; +import { AbstractQueryInterface } from '@sequelize/core'; +import { AbstractQueryInterfaceInternal } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-interface-internal.js'; +import type { OracleDialect } from './dialect.js'; + +export class OracleQueryInterfaceTypescript< + Dialect extends OracleDialect = OracleDialect, +> extends AbstractQueryInterface { + readonly #internalQueryInterface: AbstractQueryInterfaceInternal; + + constructor(dialect: Dialect, internalQueryInterface?: AbstractQueryInterfaceInternal) { + internalQueryInterface ??= new AbstractQueryInterfaceInternal(dialect); + + super(dialect, internalQueryInterface); + this.#internalQueryInterface = internalQueryInterface; + } + + async fetchDatabaseVersion(options?: FetchDatabaseVersionOptions): Promise { + const payload = await this.#internalQueryInterface.fetchDatabaseVersionRaw<{ + VERSION_FULL: string; + }>(options); + + return payload.VERSION_FULL; + } + + async dropAllTables(options?: QiDropAllTablesOptions | undefined): Promise { + const skip = options?.skip || []; + const allTables = await this.listTables(options); + const tableNames = allTables.filter(tableName => !skip.includes(tableName.tableName)); + + const dropOptions = { ...options }; + // enable "cascade" by default if supported by this dialect + if (this.sequelize.dialect.supports.dropTable.cascade && dropOptions.cascade === undefined) { + dropOptions.cascade = true; + } + + // Drop all the tables loop to avoid deadlocks and timeouts + for (const tableName of tableNames) { + // eslint-disable-next-line no-await-in-loop + await this.dropTable(tableName, dropOptions); + } + } +} diff --git a/packages/oracle/src/query-interface.d.ts b/packages/oracle/src/query-interface.d.ts new file mode 100644 index 000000000000..ce2c71a80cb3 --- /dev/null +++ b/packages/oracle/src/query-interface.d.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +import type { OracleDialect } from './dialect.js'; +import { OracleQueryInterfaceTypescript } from './query-interface-typescript.internal.ts'; + +export class OracleQueryInterface< + Dialect extends OracleDialect = OracleDialect, +> extends OracleQueryInterfaceTypescript {} diff --git a/packages/oracle/src/query-interface.js b/packages/oracle/src/query-interface.js new file mode 100644 index 000000000000..50e106879b48 --- /dev/null +++ b/packages/oracle/src/query-interface.js @@ -0,0 +1,93 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +import { QueryTypes } from '@sequelize/core'; +import { assertNoReservedBind } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/sql.js'; + +const intersection = require('lodash/intersection'); +const uniq = require('lodash/uniq'); +const { OracleQueryInterfaceTypescript } = require('./query-interface-typescript.internal'); + +export class OracleQueryInterface extends OracleQueryInterfaceTypescript { + async upsert(tableName, insertValues, updateValues, where, options) { + if (options.bind) { + assertNoReservedBind(options.bind); + } + + 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.getIndexes()) + .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; + }, {}); + } + + if (typeof tableName === 'object') { + tableName = tableName.tableName; + } + + 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); + } +} diff --git a/packages/oracle/src/query.d.ts b/packages/oracle/src/query.d.ts new file mode 100644 index 000000000000..97668d29ed21 --- /dev/null +++ b/packages/oracle/src/query.d.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +import { AbstractQuery } from '@sequelize/core'; + +export class OracleQuery extends AbstractQuery {} diff --git a/packages/oracle/src/query.js b/packages/oracle/src/query.js new file mode 100644 index 000000000000..256accc5d73c --- /dev/null +++ b/packages/oracle/src/query.js @@ -0,0 +1,751 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +import { + AbstractQuery, + DatabaseError, + ForeignKeyConstraintError, + UniqueConstraintError, + UnknownConstraintError, + ValidationErrorItem, +} from '@sequelize/core'; +import { logger } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/logger.js'; +import { nameIndex } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/string.js'; +import extend from 'lodash/extend'; +import isPlainObject from 'lodash/isPlainObject'; +import mapKeys from 'lodash/mapKeys'; +import mapValues from 'lodash/mapValues'; +import reduce from 'lodash/reduce'; +import toPairs from 'lodash/toPairs'; +import oracledb from 'oracledb'; + +const debug = logger.debugContext('sql:oracle'); + +export class OracleQuery extends AbstractQuery { + constructor(connection, sequelize, options) { + super(connection, sequelize, options); + this.options = extend( + { + // eslint-disable-next-line no-console + logging: console.log, + plain: false, + raw: false, + }, + options || {}, + ); + + this.checkLoggingOption(); + this.outFormat = options.outFormat || oracledb.OBJECT; + } + + getInsertIdField() { + return 'id'; + } + + getExecOptions() { + const execOpts = { outFormat: this.outFormat, autoCommit: this.autoCommit }; + + if (this.model && this.isSelectQuery()) { + const fInfo = {}; + const keys = Object.keys(this.model.tableAttributes); + for (const key of keys) { + const keyValue = this.model.tableAttributes[key]; + if (keyValue.type.getDataTypeId() === 'DECIMAL') { + fInfo[key] = { type: oracledb.STRING }; + } + + // Fetching BIGINT as string since, node-oracledb doesn't support JS BIGINT yet + if (keyValue.type.getDataTypeId() === 'BIGINT') { + fInfo[key] = { type: oracledb.STRING }; + } + } + + if (fInfo) { + execOpts.fetchInfo = fInfo; + } + } + + return execOpts; + } + + /** + * convert binding values for unsupported + * types in connector library + * + * @param {string} bindingDictionary a string representing the key to scan + * @private + */ + _convertBindAttributes(bindingDictionary) { + if (this.model && this.options[bindingDictionary]) { + // check against model if we have some BIGINT + const keys = Object.keys(this.model.tableAttributes); + for (const key of keys) { + const keyValue = this.model.tableAttributes[key]; + if (keyValue.type.getDataTypeId() === 'BIGINT') { + const oldBinding = this.options[bindingDictionary][key]; + if (oldBinding) { + this.options[bindingDictionary][key] = { + ...oldBinding, + type: oracledb.STRING, + maxSize: 10_000_000, // TOTALLY ARBITRARY Number to prevent query failure + }; + } + } + } + } + } + + async run(sql, parameters) { + // We set the oracledb + const complete = this._logQuery(sql, debug, parameters); + const outParameters = []; + const bindParameters = []; + const bindDef = []; + + // eslint-disable-next-line unicorn/prefer-regexp-test + 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)) + ) { + this._convertBindAttributes('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 (Array.isArray(parameters) || isPlainObject(parameters)) { + if (this.options.executeMany) { + // Constructing BindDefs for ExecuteMany call + // Building the bindDef for in and out binds + this._convertBindAttributes('inbindAttributes'); + bindDef.push(...Object.values(this.options.inbindAttributes)); + // eslint-disable-next-line unicorn/no-array-push-push + 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 (this.sql.startsWith('BEGIN TRANSACTION')) { + this.autocommit = false; + + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.resolve(); + } + + if (this.sql.startsWith('SET AUTOCOMMIT ON')) { + this.autocommit = true; + + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.resolve(); + } + + if (this.sql.startsWith('SET AUTOCOMMIT OFF')) { + this.autocommit = false; + + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.resolve(); + } + + if (this.sql.startsWith('DECLARE x NUMBER')) { + // Calling a stored procedure for bulkInsert with NO attributes, returns nothing + if (this.autoCommit === undefined) { + if (this.connection.uuid) { + this.autoCommit = false; + } else { + this.autoCommit = true; + } + } + + try { + await this.connection.execute(this.sql, this.bindParameters, { + autoCommit: this.autoCommit, + }); + + return Object.create(null); + } catch (error) { + throw this.formatError(error); + } finally { + complete(); + } + } + + if (this.sql.startsWith('BEGIN')) { + // Call to stored procedures - BEGIN TRANSACTION has been treated before + if (this.autoCommit === undefined) { + if (this.connection.uuid) { + this.autoCommit = false; + } else { + this.autoCommit = true; + } + } + + try { + const result = await this.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 (this.sql.startsWith('COMMIT TRANSACTION')) { + try { + await this.connection.commit(); + + return Object.create(null); + } catch (error) { + throw this.formatError(error); + } finally { + complete(); + } + } + + if (this.sql.startsWith('ROLLBACK TRANSACTION')) { + try { + await this.connection.rollback(); + + return Object.create(null); + } catch (error) { + throw this.formatError(error); + } finally { + complete(); + } + } + + if (this.sql.startsWith('SET TRANSACTION')) { + try { + await this.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 (this.autoCommit === undefined) { + if (this.connection.uuid) { + this.autoCommit = false; + } else { + this.autoCommit = true; + } + } + + // inbind parameters added byname. merge them + 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 + ? 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); + } catch (error) { + throw this.formatError(error); + } finally { + complete(); + } + } + + /** + * 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) { + const modelDefinition = this.model.modelDefinition; + this._getAttributeMap(attrsMap, modelDefinition.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) { + const modelDefinition = this.model.modelDefinition; + result = result.map(row => { + return mapValues(row, (value, key) => { + if (modelDefinition.rawAttributes[key] && modelDefinition.rawAttributes[key].type) { + let typeid = modelDefinition.rawAttributes[key].type.toLocaleString(); + if (modelDefinition.rawAttributes[key].type.getDataTypeId() === '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.includes('(') && + modelDefinition.rawAttributes[key].type.getDataTypeId() !== 'BOOLEAN' + ) { + typeid = typeid.slice(0, typeid.indexOf('(')); + } + + const parser = this.sequelize.dialect.getParserForDatabaseDataType(typeid); + if ((value !== null) & parser) { + value = parser(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.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]].modelDefinition.rawAttributes, + ); + } + + data.rows.forEach(_result => { + if (_result.Default) { + _result.Default = _result.Default.replace(`('`, '') + .replace(`')`, '') + // eslint-disable-next-line unicorn/prefer-string-replace-all + .replace(/'/g, ''); /* jshint ignore: line */ + } + + // no need to add additional entries for column already in. + if (!(modelAttributes[_result.COLUMN_NAME] in result) && !(_result.COLUMN_NAME in result)) { + let key = modelAttributes[_result.COLUMN_NAME]; + if (!key) { + key = _result.COLUMN_NAME; + } + + result[key] = { + type: _result.DATA_TYPE.toUpperCase(), + // eslint-disable-next-line no-unneeded-ternary + allowNull: _result.NULLABLE === 'N' ? false : true, + defaultValue: undefined, + primaryKey: _result.CONSTRAINT_TYPE === 'P', + }; + } + }); + } 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.isDeleteQuery()) { + result = data.rowsAffected; + } 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]; + } + + // eslint-disable-next-line unicorn/prefer-at + obj.isUpdate = data[data.length - 1]; + data = obj; + // eslint-disable-next-line eqeqeq + 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[key] = result[key]; + } + + return constraint; + }); + } + + 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 = []; + let message = 'Validation error'; + let uniqueKey = null; + + if (this.model) { + const uniqueKeys = this.model.getIndexes(); + + uniqueKey = uniqueKeys.find(key => { + // We check directly AND with quotes -> "a"" === a || "a" === "a" + return ( + key.name.toUpperCase() === match[1].toUpperCase() || + key.name.toUpperCase() === `"${match[1].toUpperCase()}"` + ); + }); + + if (uniqueKey) { + fields = uniqueKey.fields; + } + + // eslint-disable-next-line no-implicit-coercion + if (uniqueKey && !!uniqueKey.msg) { + message = uniqueKey.msg; + } + + fields.forEach(field => { + errors.push( + new ValidationErrorItem( + this.getUniqueConstraintErrorMessage(field), + 'unique violation', + field, + null, + ), + ); + }); + } + + return new UniqueConstraintError({ + message, + errors, + cause: 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 ForeignKeyConstraintError({ + fields: null, + index: match[1], + cause: err, + }); + } + + // ORA-02443: Cannot drop constraint - nonexistent constraint + match = err.message.match(/ORA-02443/); + if (match && match.length > 0) { + return new UnknownConstraintError(match[1]); + } + + return new DatabaseError(err); + } + + isShowIndexesQuery() { + // eslint-disable-next-line unicorn/prefer-includes + return this.sql.indexOf('SELECT i.index_name,i.table_name, i.column_name, u.uniqueness') > -1; + } + + isSelectCountQuery() { + return this.sql.toUpperCase().includes('SELECT COUNT('); + } + + 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', + primary: indexRecord.CONSTRAINT_TYPE === 'P', + name: indexRecord.INDEX_NAME, + tableName: indexRecord.TABLE_NAME.toLowerCase(), + type: undefined, + }; + acc[indexRecord.INDEX_NAME].fields = []; + } + + // We create the fields + acc[indexRecord.INDEX_NAME].fields.push({ + name: indexRecord.COLUMN_NAME, + length: undefined, + order: indexRecord.DESCEND, + collate: undefined, + }); + }); + + const returnIndexes = []; + const accKeys = Object.keys(acc); + for (const accKey of accKeys) { + const columns = {}; + columns.fields = acc[accKey].fields; + columns.unique = acc[accKey].unique; + // We are generating index field name in the format sequelize expects + // to avoid creating a unique index on auto-generated index name + // eslint-disable-next-line unicorn/prefer-regexp-test + if (acc[accKey].name.match(/SYS_C[0-9]*/)) { + acc[accKey].name = nameIndex(columns, acc[accKey].tableName).name; + } + + // eslint-disable-next-line array-callback-return + acc[accKey].fields.map(field => { + field.attribute = field.name; + delete field.name; + }); + returnIndexes.push(acc[accKey]); + } + + return returnIndexes; + } + + handleInsertQuery(results, metaData) { + if (this.instance && results && 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 modelDefinition = this.model.modelDefinition; + const autoIncrementField = modelDefinition.autoIncrementAttributeName; + let id = null; + let autoIncrementAlias = null; + + if ( + Object.hasOwn(modelDefinition.rawAttributes, autoIncrementField) && + modelDefinition.rawAttributes[autoIncrementField].field !== undefined + ) { + autoIncrementAlias = modelDefinition.rawAttributes[autoIncrementField].field; + } + + id = id || (results && results[0][this.getInsertIdField()]); + id = id || (metaData && metaData[this.getInsertIdField()]); + id = id || (results && results[0][autoIncrementField]); + id = id || (autoIncrementAlias && results && results[0][autoIncrementAlias]); + + this.instance[autoIncrementField] = id; + } + } +} diff --git a/packages/oracle/tsconfig.json b/packages/oracle/tsconfig.json new file mode 100644 index 000000000000..cfbb24587f8d --- /dev/null +++ b/packages/oracle/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig-preset.json", + "compilerOptions": { + "outDir": "./lib", + "rootDir": "./src" + }, + "include": ["./src/**/*.ts"] +} diff --git a/packages/oracle/typedoc.json b/packages/oracle/typedoc.json new file mode 100644 index 000000000000..a6500efb0151 --- /dev/null +++ b/packages/oracle/typedoc.json @@ -0,0 +1,5 @@ +{ + "extends": ["../../typedoc.base.json"], + "entryPoints": ["src/index.ts"], + "excludeExternals": true +} diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md deleted file mode 100644 index a2a340e9bbc7..000000000000 --- a/packages/utils/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [7.0.0-alpha.46](https://github.com/sequelize/sequelize/compare/v7.0.0-alpha.45...v7.0.0-alpha.46) (2025-03-22) - -### Bug Fixes - -- **cli:** remove redundant types export in package.json ([#17781](https://github.com/sequelize/sequelize/issues/17781)) ([2391263](https://github.com/sequelize/sequelize/commit/2391263eaa09ae8c3fe1ce624b3f696ccfae8501)) -- **core:** fix issues with composite PK in `findByPk` ([#17747](https://github.com/sequelize/sequelize/issues/17747)) ([dd587cb](https://github.com/sequelize/sequelize/commit/dd587cb86a1b636cdc9cc490c9325e2f6e7640a8)) -- update typescript to v5.8.2 ([#17728](https://github.com/sequelize/sequelize/issues/17728)) ([6c5a82d](https://github.com/sequelize/sequelize/commit/6c5a82dbc82ec45bbe85112c51e1b496f3f7dbaa)) - -### Features - -- **core:** add `sql.join` & improve `sql.identifier` ([#17744](https://github.com/sequelize/sequelize/issues/17744)) ([e914861](https://github.com/sequelize/sequelize/commit/e914861c084ef0ed8f12ca7b59be4965326e9641)) diff --git a/packages/validator-js/CHANGELOG.md b/packages/validator-js/CHANGELOG.md deleted file mode 100644 index 4c1d58f0d789..000000000000 --- a/packages/validator-js/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [7.0.0-alpha.46](https://github.com/sequelize/sequelize/compare/v7.0.0-alpha.45...v7.0.0-alpha.46) (2025-03-22) - -**Note:** Version bump only for package @sequelize/validator.js diff --git a/test/esm-named-exports.test.js b/test/esm-named-exports.test.js index f9aa4f3533e2..cd2ac968f305 100644 --- a/test/esm-named-exports.test.js +++ b/test/esm-named-exports.test.js @@ -64,6 +64,7 @@ const ignoredCjsKeysMap = { '@sequelize/mariadb': ['__esModule'], '@sequelize/mssql': ['__esModule'], '@sequelize/mysql': ['__esModule'], + '@sequelize/oracle': ['__esModule'], '@sequelize/postgres': ['__esModule'], '@sequelize/snowflake': ['__esModule'], '@sequelize/sqlite3': ['__esModule'], diff --git a/yarn.lock b/yarn.lock index 77d79d9b07ad..adf4a75a3dab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3009,6 +3009,24 @@ __metadata: languageName: unknown linkType: soft +"@sequelize/oracle@workspace:packages/oracle": + version: 0.0.0-use.local + resolution: "@sequelize/oracle@workspace:packages/oracle" + dependencies: + "@sequelize/core": "workspace:*" + "@sequelize/utils": "workspace:*" + "@types/chai": "npm:4.3.20" + "@types/mocha": "npm:10.0.10" + "@types/oracledb": "npm:^6.6.0" + chai: "npm:4.5.0" + dayjs: "npm:^1.11.18" + lodash: "npm:^4.17.21" + mocha: "npm:11.7.1" + oracledb: "npm:^6.8.0" + semver: "npm:^7.7.1" + languageName: unknown + linkType: soft + "@sequelize/postgres@workspace:packages/postgres": version: 0.0.0-use.local resolution: "@sequelize/postgres@workspace:packages/postgres" @@ -4107,6 +4125,15 @@ __metadata: languageName: node linkType: hard +"@types/oracledb@npm:^6.6.0": + version: 6.6.0 + resolution: "@types/oracledb@npm:6.6.0" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/98418f4d9f86113e7f1b3e5d8bc1fb28dcb94354685c62492009795171c10d2379e234a33a9af3c5af5f5559eac55498003ea3eee4e102b52ab5e3eab3bb8504 + languageName: node + linkType: hard + "@types/pg@npm:^8.11.14": version: 8.11.14 resolution: "@types/pg@npm:8.11.14" @@ -12742,6 +12769,13 @@ __metadata: languageName: node linkType: hard +"oracledb@npm:^6.8.0": + version: 6.8.0 + resolution: "oracledb@npm:6.8.0" + checksum: 10c0/344e3cb641c60896d618d9f4d080b071a199afd98123e0a2e65ca4577c91b6ca383acf85a904c9ac349c2ffa5baa7605001385c430864509efdc55fb86e54799 + languageName: node + linkType: hard + "os-tmpdir@npm:~1.0.2": version: 1.0.2 resolution: "os-tmpdir@npm:1.0.2"