├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ ├── helpers │ └── put-our-patches-in-downloaded-sequelize.js │ ├── ignore_tests │ ├── model_geometry.txt │ ├── shared │ │ ├── belongs_to.txt │ │ ├── belongs_to_many.txt │ │ ├── change_column.txt │ │ ├── cls.txt │ │ ├── connection_manager.txt │ │ ├── create_table.txt │ │ ├── dao.txt │ │ ├── data_types.txt │ │ ├── dialects_data_types.txt │ │ ├── dialects_postgres_error.txt │ │ ├── dialects_query.txt │ │ ├── dialects_query_interface.txt │ │ ├── has_many.txt │ │ ├── include_find_all.txt │ │ ├── include_find_and_count_all.txt │ │ ├── instance_to_json.txt │ │ ├── instance_validations.txt │ │ ├── json.txt │ │ ├── model.txt │ │ ├── model_attributes_field.txt │ │ ├── model_bulk_create.txt │ │ ├── model_create.txt │ │ ├── model_find_all.txt │ │ ├── model_find_all_group.txt │ │ ├── model_find_all_grouped_limit.txt │ │ ├── model_find_all_order.txt │ │ ├── model_find_one.txt │ │ ├── model_geometry.txt │ │ ├── model_increment.txt │ │ ├── multiple_level_filters.txt │ │ ├── operators.txt │ │ ├── pool.txt │ │ ├── query_interface.txt │ │ ├── remove_column.txt │ │ ├── scope.txt │ │ ├── sequelize.txt │ │ ├── sequelize_deferrable.txt │ │ ├── sequelize_query.txt │ │ ├── sync.txt │ │ └── transactions.txt │ └── v5 │ │ ├── create.txt │ │ ├── transactions.txt │ │ └── upsert.txt │ └── readme.md ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .teamcity ├── Cockroach_Sequelize │ ├── Project.kt │ ├── buildTypes │ │ └── Cockroach_Sequelize_SequelizeUnitTests.kt │ ├── pluginData │ │ └── plugin-settings.xml │ └── settings.kts └── pom.xml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── source ├── index.js ├── patch-upsert-v5.js ├── patches-v5.js ├── patches-v6.js ├── telemetry.js └── version_helper.js ├── tests ├── basic_usage_test.js ├── belongs_to_many_test.js ├── belongs_to_test.js ├── change_column_test.js ├── cls_test.js ├── connection_manager_test.js ├── create-table_test.js ├── dao_test.js ├── data_types_test.js ├── dialects_data_types_test.js ├── dialects_postgres_error_test.js ├── dialects_query_interface_test.js ├── dialects_query_test.js ├── drop_enum_test.js ├── enum_test.js ├── find_or_create_test.js ├── has_many_test.js ├── helper.js ├── include_find_all_test.js ├── include_find_and_count_all_test.js ├── instance_to_json_test.js ├── instance_validations_test.js ├── int_test.js ├── json_test.js ├── model_attributes_field_test.js ├── model_bulk_create_test.js ├── model_create_test.js ├── model_find_all_group_test.js ├── model_find_all_grouped_limit_test.js ├── model_find_all_order_test.js ├── model_find_all_test.js ├── model_find_one_test.js ├── model_geometry_test.js ├── model_increment_test.js ├── model_test.js ├── model_update_test.js ├── multiple_level_filters_test.js ├── operators_test.js ├── pool_test.js ├── query_interface_test.js ├── remove_column_test.js ├── run_tests │ ├── getTestsToIgnore.js │ └── runTests.js ├── scope_test.js ├── sequelize_deferrable_test.js ├── sequelize_query_test.js ├── sequelize_test.js ├── support.js ├── sync_test.js ├── transaction_test.js └── upsert_test.js └── types └── index.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Cockroach Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. See the License for the specific language governing 13 | # permissions and limitations under the License. 14 | 15 | name: CI 16 | on: [push, pull_request] 17 | 18 | jobs: 19 | test: 20 | # Execute our own tests 21 | name: Main - CRDB ${{ matrix.cockroachdb-docker-version }}, Sequelize v${{ matrix.sequelize-version }} 22 | strategy: 23 | matrix: 24 | # TODO(richardjcai): Re-enable tests for v5 after we get v6 suite passing. 25 | # Focus on getting tests passing for v6 test suite. 26 | sequelize-version: [5, 6] 27 | cockroachdb-docker-version: ["cockroachdb/cockroach:v21.2.4", "cockroachdb/cockroach:v21.1.13"] 28 | fail-fast: false 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Start a single CockroachDB instance (${{ matrix.cockroachdb-docker-version }}) with docker 32 | env: 33 | CONTAINER_ENTRYPOINT: ${{ 'cockroach' }} 34 | run: | 35 | echo $CONTAINER_ENTRYPOINT 36 | docker pull ${{ matrix.cockroachdb-docker-version }} 37 | docker run -d --name roach --hostname roach -p 26257:26257 -p 8080:8080 ${{ matrix.cockroachdb-docker-version }} start-single-node --insecure 38 | sudo apt update && sudo apt install wait-for-it -y 39 | wait-for-it -h localhost -p 26257 40 | docker exec roach bash -c "echo 'CREATE DATABASE sequelize_test;' | $CONTAINER_ENTRYPOINT sql --insecure" 41 | 42 | - name: Checkout the repository 43 | uses: actions/checkout@v2 44 | 45 | - name: Setup Node.js 12.x 46 | uses: actions/setup-node@v1 47 | with: 48 | node-version: 12.x 49 | 50 | - name: Install dependencies 51 | run: npm install 52 | 53 | - name: Install Sequelize v${{ matrix.sequelize-version }} 54 | run: npm install --save sequelize@^${{ matrix.sequelize-version }} 55 | 56 | - name: Run tests 57 | run: npm test --crdb_version=${{ matrix.cockroachdb-docker-version }} 58 | 59 | sequelize-postgres-integration-tests: 60 | # Execute Sequelize integration tests for postgres 61 | # This one will spawn 219 jobs 62 | if: ${{ !endsWith(github.event.head_commit.message, '[[skip sequelize integration tests]]') }} 63 | name: (${{ matrix.sequelize-branch }}) ${{ matrix.test-path }} 64 | strategy: 65 | matrix: 66 | # TODO(richardjcai): Re-enable tests for v5 after we get v6 suite passing. 67 | # Focus on getting tests passing for v6 test suite. 68 | sequelize-branch: [v5, v6] 69 | test-path: [associations/alias, associations/belongs-to-many, associations/belongs-to, associations/has-many, associations/has-one, associations/multiple-level-filters, associations/scope, associations/self, cls, configuration, data-types, dialects/abstract/connection-manager, dialects/postgres/associations, dialects/postgres/connection-manager, dialects/postgres/dao, dialects/postgres/data-types, dialects/postgres/error, dialects/postgres/hstore, dialects/postgres/query-interface, dialects/postgres/query, dialects/postgres/range, dialects/postgres/regressions, error, hooks/associations, hooks/bulkOperation, hooks/count, hooks/create, hooks/destroy, hooks/find, hooks/hooks, hooks/restore, hooks/updateAttributes, hooks/upsert, hooks/validate, include/findAll, include/findAndCountAll, include/findOne, include/limit, include/paranoid, include/schema, include/separate, include, instance/decrement, instance/destroy, instance/increment, instance/reload, instance/save, instance/to-json, instance/update, instance/values, instance, instance.validations, json, model/attributes/field, model/attributes/types, model/attributes, model/bulk-create/include, model/bulk-create, model/count, model/create/include, model/create, model/findAll/group, model/findAll/groupedLimit, model/findAll/order, model/findAll/separate, model/findAll, model/findOne, model/findOrBuild, model/geography, model/geometry, model/increment, model/json, model/optimistic_locking, model/paranoid, model/schema, model/scope/aggregate, model/scope/associations, model/scope/count, model/scope/destroy, model/scope/find, model/scope/findAndCountAll, model/scope/merge, model/scope/update, model/scope, model/searchPath, model/sum, model/sync, model/update, model/upsert, model, operators, pool, query-interface/changeColumn, query-interface/createTable, query-interface/describeTable, query-interface/dropEnum, query-interface/removeColumn, query-interface, replication, schema, sequelize/deferrable, sequelize/log, sequelize, sequelize.transaction, timezone, transaction, trigger, utils, vectors] 70 | include: 71 | - 72 | # Luckily the test files are the same in v5 and v6, except for one extra file in v6: 73 | sequelize-branch: v6 74 | test-path: sequelize/query 75 | fail-fast: false 76 | runs-on: ubuntu-latest 77 | env: 78 | DIALECT: postgres 79 | SEQ_PORT: 26257 80 | SEQ_USER: root 81 | SEQ_PW: '' 82 | SEQ_DB: sequelize_test 83 | steps: 84 | - name: Start a single CockroachDB instance (v21.2.4) with docker 85 | run: | 86 | docker pull cockroachdb/cockroach:v21.2.4 87 | docker run -d --name roach --hostname roach -p 26257:26257 -p 8080:8080 cockroachdb/cockroach:v21.2.4 start-single-node --insecure 88 | sudo apt update && sudo apt install wait-for-it -y 89 | wait-for-it -h localhost -p 26257 90 | docker exec roach bash -c 'echo "CREATE DATABASE sequelize_test;" | cockroach sql --insecure' 91 | 92 | - name: Checkout `sequelize-cockroachdb` repository 93 | uses: actions/checkout@v2 94 | 95 | - name: Setup Node.js 12.x 96 | uses: actions/setup-node@v1 97 | with: 98 | node-version: 12.x 99 | 100 | - name: Install `sequelize-cockroachdb` dependencies 101 | run: npm install 102 | 103 | - name: Fetch Sequelize source code directly from GitHub and unzip it into `.downloaded-sequelize` 104 | run: | 105 | wget https://github.com/sequelize/sequelize/archive/${{ matrix.sequelize-branch }}.zip 106 | unzip ${{ matrix.sequelize-branch }}.zip -d temp-unzip-out 107 | mv temp-unzip-out/* .downloaded-sequelize 108 | rmdir temp-unzip-out 109 | 110 | - name: Install Sequelize dependencies 111 | working-directory: ./.downloaded-sequelize 112 | run: npm install --ignore-scripts 113 | 114 | - name: Provide the `sequelize-cockroachdb` patches 115 | # This script needs `fs-jetpack` as an extra dependency 116 | run: | 117 | npm install --no-save fs-jetpack 118 | node .github/workflows/helpers/put-our-patches-in-downloaded-sequelize.js 119 | cat .downloaded-sequelize/.cockroachdb-patches/index.js 120 | 121 | - name: Copy over the dependencies needed by `sequelize-cockroachdb` 122 | # (we use `npm ls --prod=true` to avoid copying unnecessary dev-dependencies) 123 | run: | 124 | mkdir -p .downloaded-sequelize/.cockroachdb-patches/node_modules 125 | npm ls --prod=true --parseable=true | grep node_modules | xargs -I{} cp -r {} .downloaded-sequelize/.cockroachdb-patches/node_modules/. 126 | 127 | - name: Run integration tests at '${{ matrix.test-path }}' for Sequelize ${{ matrix.sequelize-branch }} 128 | working-directory: ./.downloaded-sequelize 129 | run: TEST_PATH=${{ matrix.test-path }} node ./../tests/run_tests/runTests.js 130 | -------------------------------------------------------------------------------- /.github/workflows/helpers/put-our-patches-in-downloaded-sequelize.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | const jetpack = require('fs-jetpack'); 16 | 17 | // This function wraps our source code, patching our `require` calls, to make them point to the correct relative path. 18 | function copyFileWrapping(sourcePath, destinationPath) { 19 | const contents = jetpack.read(sourcePath); 20 | jetpack.write( 21 | destinationPath, 22 | ` 23 | 'use strict'; 24 | const originalRequire = require; 25 | require = modulePath => originalRequire(modulePath.replace(/^sequelize\\b/, '..')); 26 | console.log('[INFO] Applying "sequelize-cockroachdb" patches! (${JSON.stringify(destinationPath)})'); 27 | (() => { 28 | // ------------------------------------------------------------------------------------ 29 | ${contents} 30 | // ------------------------------------------------------------------------------------ 31 | })(); 32 | ` 33 | ); 34 | } 35 | 36 | // Wrap our source code files and write the wrapped versions into `.downloaded-sequelize/.cockroachdb-patches/` 37 | for (const filename of jetpack.list('source')) { 38 | copyFileWrapping(`source/${filename}`, `.downloaded-sequelize/.cockroachdb-patches/${filename}`); 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/model_geometry.txt: -------------------------------------------------------------------------------- 1 | GEOMETRY 2 | GEOMETRY(POINT) 3 | GEOMETRY(LINESTRING) 4 | GEOMETRY(POLYGON) 5 | sql injection attacks 6 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/belongs_to.txt: -------------------------------------------------------------------------------- 1 | should set foreignKey on foreign table 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/belongs_to_many.txt: -------------------------------------------------------------------------------- 1 | using scope to set associations 2 | updating association via set associations with scope 3 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/change_column.txt: -------------------------------------------------------------------------------- 1 | should support schemas 2 | should change columns 3 | should work with enums \(case 1\) 4 | should work with enums \(case 2\) 5 | should work with enums with schemas 6 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/cls.txt: -------------------------------------------------------------------------------- 1 | automagically uses the transaction in all calls 2 | automagically uses the transaction in all calls with async\/await 3 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/connection_manager.txt: -------------------------------------------------------------------------------- 1 | should fetch regular dynamic oids and create parsers 2 | should fetch range dynamic oids and create parsers 3 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/create_table.txt: -------------------------------------------------------------------------------- 1 | should create a auto increment primary key 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/dao.txt: -------------------------------------------------------------------------------- 1 | \[POSTGRES Specific\] DAO 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/data_types.txt: -------------------------------------------------------------------------------- 1 | calls parse and stringify for JSON 2 | calls parse and bindParam for HSTORE 3 | calls parse and bindParam for RANGE 4 | calls parse and stringify for INTEGER 5 | calls parse and stringify for CIDR 6 | calls parse and stringify for CITEXT 7 | calls parse and stringify for MACADDR 8 | calls parse and stringify for TSVECTOR 9 | should return Int4 range properly #5747 10 | should allow date ranges to be generated with default bounds inclusion #8176 11 | should allow date ranges to be generated using a single range expression to define bounds inclusion #8176 12 | should allow date ranges to be generated using a composite range expression #8176 13 | should correctly return ranges when using predicates that define bounds inclusion #8176 14 | calls parse and bindParam for GEOMETRY 15 | should store and parse IEEE floating point literals \(NaN and Infinity\) 16 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/dialects_data_types.txt: -------------------------------------------------------------------------------- 1 | should be able to create and update records with Infinity/-Infinity 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/dialects_postgres_error.txt: -------------------------------------------------------------------------------- 1 | \[POSTGRES Specific\] ExclusionConstraintError 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/dialects_query.txt: -------------------------------------------------------------------------------- 1 | should throw due to alias being truncated 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/dialects_query_interface.txt: -------------------------------------------------------------------------------- 1 | \[POSTGRES Specific\] QueryInterface 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/has_many.txt: -------------------------------------------------------------------------------- 1 | supports schemas 2 | should fetch associations for multiple instances with limit and order and a belongsTo relation 3 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/include_find_all.txt: -------------------------------------------------------------------------------- 1 | should be possible to select on columns inside a through table 2 | should be possible to select on columns inside a through table and a limit 3 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/include_find_and_count_all.txt: -------------------------------------------------------------------------------- 1 | should be able to include a required model. Result rows should match count 2 | should correctly filter, limit and sort when multiple includes and types of associations are present. 3 | should properly work with sequelize.function 4 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/instance_to_json.txt: -------------------------------------------------------------------------------- 1 | returns a response that can be stringified 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/instance_validations.txt: -------------------------------------------------------------------------------- 1 | should allow us to update specific columns without tripping the validations 2 | should enforce a unique constraint 3 | should allow a custom unique constraint error message 4 | should handle multiple unique messages correctly 5 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/json.txt: -------------------------------------------------------------------------------- 1 | should tell me that a column is json 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/model.txt: -------------------------------------------------------------------------------- 1 | allows unique on column with field aliases 2 | allows us to map the customized error message with unique constraint name 3 | should allow the user to specify indexes in options 4 | does not set deletedAt for previously destroyed instances if paranoid is true 5 | can\'t find records marked as deleted with paranoid being true 6 | can find paranoid records if paranoid is marked as false in query 7 | should include deleted associated records if include has paranoid marked as false 8 | should be able to list schemas 9 | should describeTable using the default schema settings 10 | supports multiple async transactions 11 | allows us to customize the error message for unique constraint 12 | should not fail when array contains Sequelize.or / and 13 | should not overwrite a specified deletedAt \(complex query\) by setting paranoid: false 14 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/model_attributes_field.txt: -------------------------------------------------------------------------------- 1 | reload should work 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/model_bulk_create.txt: -------------------------------------------------------------------------------- 1 | should make the auto incremented values available on the returned instances 2 | should make the auto incremented values available on the returned instances with custom fields 3 | should return auto increment primary key values 4 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/model_create.txt: -------------------------------------------------------------------------------- 1 | should make the autoincremented values available on the returned instances 2 | should make the autoincremented values available on the returned instances with custom fields 3 | is possible to use functions as default values 4 | doesn't allow case-insensitive duplicated records using CITEXT 5 | allows the creation of a TSVECTOR field 6 | TSVECTOR only allow string 7 | doesn't allow duplicated records with unique function based indexes 8 | sets auto increment fields 9 | should return autoIncrement primary key \(create\) 10 | several concurrent calls 11 | should error correctly when defaults contain a unique key and the where clause is complex 12 | should work with multiple concurrent calls 13 | should not deadlock with concurrency duplicate entries and no outer transaction 14 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/model_find_all.txt: -------------------------------------------------------------------------------- 1 | should be able to find multiple users with case-insensitive on CITEXT type 2 | should allow us to find IDs using capital letters 3 | sorts the results via id in ascending order 4 | sorts the results via id in descending order 5 | sorts the results via a date column 6 | handles offset and limit 7 | should be able to handle binary values through associations as well... 8 | should be able to handle false/true values through associations as well... 9 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/model_find_all_group.txt: -------------------------------------------------------------------------------- 1 | should correctly group with attributes, #3009 2 | should not add primary key when grouping using a belongsTo association 3 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/model_find_all_grouped_limit.txt: -------------------------------------------------------------------------------- 1 | maps attributes from a grouped limit to models 2 | maps attributes from a grouped limit to models with include 3 | works with computed order 4 | works with multiple orders 5 | works with paranoid junction models 6 | Applies limit and order correctly 7 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/model_find_all_order.txt: -------------------------------------------------------------------------------- 1 | should not throw with on NULLS LAST/NULLS FIRST 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/model_find_one.txt: -------------------------------------------------------------------------------- 1 | returns a single dao 2 | returns a single dao given a string id 3 | should make aliased attributes available 4 | should allow us to find IDs using capital letters 5 | should allow case-insensitive find on CITEXT type 6 | should allow case-sensitive find on TSVECTOR type 7 | throws error when record not found by findByPk 8 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/model_geometry.txt: -------------------------------------------------------------------------------- 1 | GEOMETRY 2 | GEOMETRY(POINT) 3 | GEOMETRY(LINESTRING) 4 | GEOMETRY(POLYGON) 5 | sql injection attacks 6 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/model_increment.txt: -------------------------------------------------------------------------------- 1 | with timestamps set to true 2 | with timestamps set to true and options.silent set to true 3 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/multiple_level_filters.txt: -------------------------------------------------------------------------------- 1 | can filter through belongsTo 2 | avoids duplicated tables in query 3 | can filter through hasMany 4 | can filter through hasMany connector 5 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/operators.txt: -------------------------------------------------------------------------------- 1 | should properly escape regular expressions 2 | should work with a case-insensitive not regexp where 3 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/pool.txt: -------------------------------------------------------------------------------- 1 | Pooling 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/query_interface.txt: -------------------------------------------------------------------------------- 1 | adds, reads and removes an index to the table 2 | works with schemas 3 | should get a list of foreign keys for the table 4 | should add, read & remove primary key constraint 5 | should add, read & remove foreign key constraint 6 | should throw non existent constraints as UnknownConstraintError 7 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/remove_column.txt: -------------------------------------------------------------------------------- 1 | should be able to remove a column with primaryKey 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/scope.txt: -------------------------------------------------------------------------------- 1 | on the through model 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/sequelize.txt: -------------------------------------------------------------------------------- 1 | truncates all models 2 | correctly handles multiple transactions 3 | fails with incorrect database credentials \(1\) 4 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/sequelize_deferrable.txt: -------------------------------------------------------------------------------- 1 | Deferrable 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/sequelize_query.txt: -------------------------------------------------------------------------------- 1 | properly bind parameters on extra retries 2 | add parameters in log sql 3 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/sync.txt: -------------------------------------------------------------------------------- 1 | should change a column if it exists in the model but is different in the database 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/shared/transactions.txt: -------------------------------------------------------------------------------- 1 | supports transactions 2 | does not try to rollback a transaction that failed upon committing with SERIALIZABLE isolation level \(#3689\) 3 | should read the most recent committed rows when using the READ COMMITTED isolation level 4 | supports for share \(i.e. `SELECT ... LOCK IN SHARE MODE`\) 5 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/v5/create.txt: -------------------------------------------------------------------------------- 1 | should release transaction when meeting errors 2 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/v5/transactions.txt: -------------------------------------------------------------------------------- 1 | do not rollback if already committed 2 | supports for share 3 | "after each" hook for "supports for share" 4 | -------------------------------------------------------------------------------- /.github/workflows/ignore_tests/v5/upsert.txt: -------------------------------------------------------------------------------- 1 | works with upsert on id 2 | works with upsert on a composite key 3 | works with upsert on a composite primary key 4 | works with BLOBs 5 | works with .field 6 | works with primary key using .field 7 | works with database functions 8 | does not update when setting current values 9 | works when two separate uniqueKeys are passed 10 | works when indexes are created via indexes array 11 | works when composite indexes are created via indexes array 12 | should return default value set by the database (upsert) 13 | works with upsert on id 14 | works for table with custom primary key field 15 | works for non incrementing primaryKey 16 | -------------------------------------------------------------------------------- /.github/workflows/readme.md: -------------------------------------------------------------------------------- 1 | # CI workflow for executing Sequelize's postgres tests targeting CockroachDB 2 | 3 | The `ci.yml` file includes a job specification called `sequelize-postgres-integration-tests` which is responsible for running Sequelize's own integration tests that were originally written for postgres but targeting CockroachDB instead. 4 | 5 | It works as follows: 6 | 7 | * Download the Sequelize source code into a temporary folder called `.downloaded-sequelize` 8 | * Install all Sequelize dependencies (in order to be able to run all Sequelize's own tests) 9 | * Copy `sequelize-cockroachdb` source code into `.downloaded-sequelize/.cockroachdb-patches/` 10 | * This is done via the `put-our-patches-in-downloaded-sequelize.js` helper file. It not only copies our source code, but also wraps it in a way that patches all our `require('sequelize')` calls to make them work inside Sequelize's source code. The `require` wrapper simply transforms arguments like `'sequelize'` into the appropriate relative path (which is `'..'` since our code is within the `.cockroach-patches` directory). 11 | * Tell Sequelize to execute our code (in `.downloaded-sequelize/.cockroachdb-patches/`) before running the tests 12 | * Run Sequelize tests 13 | * Note: if a test fails in a way that the database cannot be cleaned up afterwards, this will cause the entire test execution to abort; to minimize the impact of this, the Sequelize tests will be run separately per test file instead of running all tests at once. 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignored directories 2 | .github 3 | .teamcity 4 | *.md -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "trailingComma": "none", 6 | "bracketSpacing": true, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /.teamcity/Cockroach_Sequelize/Project.kt: -------------------------------------------------------------------------------- 1 | package Cockroach_Sequelize 2 | 3 | import Cockroach_Sequelize.buildTypes.* 4 | import jetbrains.buildServer.configs.kotlin.v10.* 5 | import jetbrains.buildServer.configs.kotlin.v10.Project 6 | import jetbrains.buildServer.configs.kotlin.v10.projectFeatures.VersionedSettings 7 | import jetbrains.buildServer.configs.kotlin.v10.projectFeatures.VersionedSettings.* 8 | import jetbrains.buildServer.configs.kotlin.v10.projectFeatures.versionedSettings 9 | 10 | object Project : Project({ 11 | uuid = "d876f2bd-f45d-4650-b9c1-4676bb86dd0b" 12 | extId = "Cockroach_Sequelize" 13 | parentId = "Cockroach" 14 | name = "Sequelize" 15 | 16 | buildType(Cockroach_Sequelize_SequelizeUnitTests) 17 | 18 | features { 19 | versionedSettings { 20 | id = "PROJECT_EXT_7" 21 | mode = VersionedSettings.Mode.ENABLED 22 | buildSettingsMode = VersionedSettings.BuildSettingsMode.PREFER_SETTINGS_FROM_VCS 23 | rootExtId = "Cockroach_HttpsGithubComCockroachdbSequelizeCockroachdbRefsHeadsMaster" 24 | showChanges = false 25 | settingsFormat = VersionedSettings.Format.KOTLIN 26 | param("credentialsStorageType", "credentialsJSON") 27 | } 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /.teamcity/Cockroach_Sequelize/buildTypes/Cockroach_Sequelize_SequelizeUnitTests.kt: -------------------------------------------------------------------------------- 1 | package Cockroach_Sequelize.buildTypes 2 | 3 | import jetbrains.buildServer.configs.kotlin.v10.* 4 | import jetbrains.buildServer.configs.kotlin.v10.triggers.VcsTrigger 5 | import jetbrains.buildServer.configs.kotlin.v10.triggers.VcsTrigger.* 6 | import jetbrains.buildServer.configs.kotlin.v10.triggers.vcs 7 | 8 | object Cockroach_Sequelize_SequelizeUnitTests : BuildType({ 9 | uuid = "a5f9a913-5926-4696-8768-a0738a6211bf" 10 | extId = "Cockroach_Sequelize_SequelizeUnitTests" 11 | name = "Sequelize Unit Tests" 12 | 13 | vcs { 14 | root("Cockroach_HttpsGithubComCockroachdbSequelizeCockroachdbRefsHeadsMaster") 15 | 16 | } 17 | 18 | triggers { 19 | vcs { 20 | } 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /.teamcity/Cockroach_Sequelize/pluginData/plugin-settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.teamcity/Cockroach_Sequelize/settings.kts: -------------------------------------------------------------------------------- 1 | package Cockroach_Sequelize 2 | 3 | import jetbrains.buildServer.configs.kotlin.v10.* 4 | 5 | /* 6 | The settings script is an entry point for defining a single 7 | TeamCity project. TeamCity looks for the 'settings.kts' file in a 8 | project directory and runs it if it's found, so the script name 9 | shouldn't be changed and its package should be the same as the 10 | project's external id. 11 | 12 | The script should contain a single call to the project() function 13 | with a Project instance or an init function as an argument, you 14 | can also specify both of them to refine the specified Project in 15 | the init function. 16 | 17 | VcsRoots, BuildTypes, and Templates of this project must be 18 | registered inside project using the vcsRoot(), buildType(), and 19 | template() methods respectively. 20 | 21 | Subprojects can be defined either in their own settings.kts or by 22 | calling the subProjects() method in this project. 23 | */ 24 | 25 | version = "2017.1" 26 | project(Cockroach_Sequelize.Project) -------------------------------------------------------------------------------- /.teamcity/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | Cockroach_Sequelize Config DSL Script 5 | 6 | 1.0.3 7 | 2017.1 8 | 9 | Cockroach_Sequelize 10 | Cockroach_Sequelize_dsl 11 | 1.0-SNAPSHOT 12 | 13 | 14 | 15 | jetbrains-all 16 | http://download.jetbrains.com/teamcity-repository 17 | 18 | true 19 | 20 | 21 | 22 | teamcity-server 23 | https://teamcity.cockroachdb.com/app/dsl-plugins-repository 24 | 25 | true 26 | 27 | 28 | 29 | 30 | 31 | 32 | JetBrains 33 | http://download.jetbrains.com/teamcity-repository 34 | 35 | 36 | 37 | 38 | . 39 | 40 | 41 | kotlin-maven-plugin 42 | org.jetbrains.kotlin 43 | ${kotlin.version} 44 | 45 | 46 | 47 | 48 | compile 49 | process-sources 50 | 51 | compile 52 | 53 | 54 | 55 | test-compile 56 | process-test-sources 57 | 58 | test-compile 59 | 60 | 61 | 62 | 63 | 64 | org.jetbrains.teamcity 65 | teamcity-configs-maven-plugin 66 | ${teamcity.dsl.version} 67 | 68 | kotlin 69 | target/generated-configs 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.jetbrains.teamcity 78 | configs-dsl-kotlin 79 | ${teamcity.dsl.version} 80 | compile 81 | 82 | 83 | org.jetbrains.teamcity 84 | configs-dsl-kotlin-plugins 85 | 1.0-SNAPSHOT 86 | pom 87 | compile 88 | 89 | 90 | org.jetbrains.kotlin 91 | kotlin-stdlib 92 | ${kotlin.version} 93 | compile 94 | 95 | 96 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 6.0.5 2 | Released January 12, 2022 3 | * Fixed a bug with importing modules from Sequelize. 4 | * Updated CockroachDB versions under test (includes v21.2 now). 5 | 6 | # Version 6.0.4 7 | Released December 16, 2021 8 | * Fixed a bug with checking the version on startup. 9 | 10 | # Version 6.0.3 11 | Released October 21, 2021 12 | * Use a deterministic ordering when introspecting enum types. 13 | * Version number telemetry now only reports the major/minor versions of Sequelize. 14 | 15 | # Version 6.0.2 16 | Released September 30, 2021 17 | * Fix a missing import that would cause an error when validating datatypes. 18 | 19 | # Version 6.0.1 20 | Released July 14, 2021 21 | * Record telemetry for sequelize-cockroachdb version in addition to the sequelize version. 22 | 23 | # Version 6.0.0 24 | Released June 14, 2021 25 | * Initial support for Sequelize 6.0 26 | * Added telemetry. The sequelize version is recorded when creating a new instance of a CockroachDB Sequelize instance. 27 | * Opt out of telemetry by specifying `cockroachdbTelemetryDisabled : true` in the `dialectOptions` object when creating a `Sequelize` object. 28 | * Example: 29 | ``` 30 | var sequelize2 = new Sequelize({ 31 | dialect: "postgres", 32 | username: "max", 33 | password: "", 34 | host: "localhost", 35 | port: 26257, 36 | database: "sequelize_test", 37 | dialectOptions: {cockroachdbTelemetryDisabled : true}, 38 | logging: false, 39 | }); 40 | ``` 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sequelize-cockroachdb 2 | 3 | [This NPM package](https://www.npmjs.com/package/sequelize-cockroachdb) makes Sequelize compatible with CockroachDB. 4 | 5 | [Learn how to build a Node.js app with CockroachDB.](https://www.cockroachlabs.com/docs/stable/build-a-nodejs-app-with-cockroachdb-sequelize.html) 6 | 7 | Please file bugs against the [sequelize-cockroachdb project](https://github.com/cockroachdb/sequelize-cockroachdb/issues/new) 8 | 9 | ## Requirements 10 | 11 | This package needs Node.js v12 or later 12 | 13 | ## Setup and run tests 14 | 15 | First make sure you have CockroachDB installed. You can run `cockroach version` to see if is installed or you can [download here](https://www.cockroachlabs.com/docs/stable/install-cockroachdb.html) 16 | 17 | Run `cockroach start-single-node --insecure --logtostderr` to start the database. If this returns `ERROR: cockroach server exited with error: unable to lookup hostname` run with `--host localhost` flag. 18 | 19 | Run `cockroach sql --insecure` to enter in SQL mode and type `CREATE DATABASE sequelize_test;` 20 | 21 | Then install the depedencies with `npm i` and `npm test` to run all tests 22 | 23 | ## Limitations 24 | 25 | ### Dealing with transactions 26 | 27 | From the [docs](https://www.cockroachlabs.com/docs/stable/transactions.html) 28 | 29 | > CockroachDB guarantees that while a transaction is pending, it is isolated from other concurrent transactions with serializable isolation. 30 | 31 | Which means that any other query made in another connection to the same [node](https://www.cockroachlabs.com/blog/how-cockroachdb-distributes-atomic-transactions/) will hang. 32 | 33 | For example: 34 | ```js 35 | const t = await this.sequelize.transaction(); 36 | await this.User.create({ name: "bob" }, { transaction: t }); 37 | await this.User.findAll({ transaction: null }); // Query will hang! 38 | ``` 39 | 40 | ### CockroachDB does not support yet: 41 | 42 | 43 | - [CITEXT](https://github.com/cockroachdb/cockroach/issues/22463) 44 | - [TSVector](https://github.com/cockroachdb/cockroach/issues/41288) 45 | - [lower](https://github.com/cockroachdb/cockroach/issues/9682?version=v20.2) function for index 46 | 47 | See `tests/model_create_test.js` to browse those implementations. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sequelize-cockroachdb", 3 | "version": "6.0.5", 4 | "description": "Support using Sequelize with CockroachDB.", 5 | "license": "Apache-2.0", 6 | "repository": "cockroachdb/sequelize-cockroachdb", 7 | "homepage": "https://www.cockroachlabs.com/docs/stable/build-a-nodejs-app-with-cockroachdb-sequelize.html", 8 | "author": "Cuong Do ", 9 | "keywords": [ 10 | "database", 11 | "sequelize", 12 | "cockroach", 13 | "cockroachdb", 14 | "orm", 15 | "object relational mapper" 16 | ], 17 | "engines": { 18 | "node": ">=12" 19 | }, 20 | "main": "source/index.js", 21 | "types": "types", 22 | "files": [ 23 | "source", 24 | "types" 25 | ], 26 | "scripts": { 27 | "lint": "prettier --write .", 28 | "test": "env CRDB_VERSION=$npm_config_crdb_version mocha --check-leaks --colors -t 300000 --reporter spec \"tests/*_test.js\"" 29 | }, 30 | "dependencies": { 31 | "lodash": "^4.17.20", 32 | "p-settle": "^4.1.1", 33 | "pg": "^8.4.1", 34 | "semver": "^7.3.2" 35 | }, 36 | "devDependencies": { 37 | "assert": "^2.0.0", 38 | "chai": "^4.2.0", 39 | "chai-as-promised": "^7.1.1", 40 | "chai-datetime": "^1.7.0", 41 | "cls-hooked": "^4.2.2", 42 | "delay": "^5.0.0", 43 | "mocha": "^8.2.0", 44 | "p-timeout": "^4.1.0", 45 | "prettier": "2.2.1", 46 | "sequelize": "^6.13.0", 47 | "sinon": "^9.2.4", 48 | "sinon-chai": "^3.5.0" 49 | }, 50 | "peerDependencies": { 51 | "sequelize": "5 - 6" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /source/patches-v5.js: -------------------------------------------------------------------------------- 1 | const { QueryInterface } = require('sequelize/lib/query-interface'); 2 | 3 | QueryInterface.prototype.__dropSchema = QueryInterface.prototype.dropSchema; 4 | 5 | QueryInterface.prototype.dropSchema = async function (tableName, options) { 6 | if (tableName === 'crdb_internal') return; 7 | 8 | await this.__dropSchema(tableName, options); 9 | }; 10 | 11 | const QueryGenerator = require('sequelize/lib/dialects/abstract/query-generator'); 12 | QueryInterface.prototype.__removeConstraint = 13 | QueryInterface.prototype.removeConstraint; 14 | 15 | QueryInterface.prototype.removeConstraint = async function ( 16 | tableName, 17 | constraintName, 18 | options 19 | ) { 20 | try { 21 | await this.__removeConstraint(tableName, constraintName, options); 22 | } catch (error) { 23 | if (error.message.includes('use DROP INDEX CASCADE instead')) { 24 | const query = QueryGenerator.prototype.removeConstraintQuery.call( 25 | this, 26 | tableName, 27 | constraintName 28 | ); 29 | const [, queryConstraintName] = query.split('DROP CONSTRAINT'); 30 | const newQuery = `DROP INDEX ${queryConstraintName} CASCADE;`; 31 | 32 | return this.sequelize.query(newQuery, options); 33 | } else throw error; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /source/patches-v6.js: -------------------------------------------------------------------------------- 1 | const { 2 | PostgresQueryInterface 3 | } = require('sequelize/lib/dialects/postgres/query-interface'); 4 | 5 | PostgresQueryInterface.prototype.__dropSchema = 6 | PostgresQueryInterface.prototype.dropSchema; 7 | 8 | PostgresQueryInterface.prototype.dropSchema = async function ( 9 | tableName, 10 | options 11 | ) { 12 | if (tableName === 'crdb_internal') return; 13 | 14 | await this.__dropSchema(tableName, options); 15 | }; 16 | 17 | PostgresQueryInterface.prototype.__removeConstraint = 18 | PostgresQueryInterface.prototype.removeConstraint; 19 | 20 | PostgresQueryInterface.prototype.removeConstraint = async function ( 21 | tableName, 22 | constraintName, 23 | options 24 | ) { 25 | try { 26 | await this.__removeConstraint(tableName, constraintName, options); 27 | } catch (error) { 28 | if (error.message.includes('use DROP INDEX CASCADE instead')) { 29 | const query = this.queryGenerator.removeConstraintQuery( 30 | tableName, 31 | constraintName 32 | ); 33 | const [, queryConstraintName] = query.split('DROP CONSTRAINT'); 34 | const newQuery = `DROP INDEX ${queryConstraintName} CASCADE;`; 35 | 36 | return this.sequelize.query(newQuery, options); 37 | } else throw error; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /source/telemetry.js: -------------------------------------------------------------------------------- 1 | const { Sequelize, QueryTypes } = require('sequelize'); 2 | 3 | const version_helper = require('./version_helper.js') 4 | 5 | //// Log telemetry for Sequelize ORM. 6 | Sequelize.addHook('afterInit', async (connection) => { 7 | try { 8 | if (connection.options.dialectOptions) { 9 | var telemetryDisabled = connection.options.dialectOptions.cockroachdbTelemetryDisabled 10 | if (telemetryDisabled) { 11 | return 12 | } 13 | } 14 | 15 | // crdb_internal.increment_feature_counter is only available on 21.1 and above. 16 | if (!(await version_helper.IsCockroachVersion21_1Plus(connection))) { 17 | return 18 | } 19 | var sequelizeVersion = version_helper.GetSequelizeVersion() 20 | var sequelizeVersionSeries = version_helper.GetVersionSeries(sequelizeVersion.version) 21 | var sequelizeVersionStr = (sequelizeVersionSeries===null)?sequelizeVersion:sequelizeVersionSeries 22 | await connection.query(`SELECT crdb_internal.increment_feature_counter(concat('Sequelize ', :SequelizeVersionString))`, 23 | { replacements: { SequelizeVersionString: sequelizeVersionStr }, type: QueryTypes.SELECT }) 24 | 25 | var adapterVersion = version_helper.GetAdapterVersion() 26 | await connection.query(`SELECT crdb_internal.increment_feature_counter(concat('sequelize-cockroachdb ', :AdapterVersion))`, 27 | { replacements: { AdapterVersion: adapterVersion.version }, type: QueryTypes.SELECT }) 28 | } catch (error) { 29 | console.info("Could not record telemetry.\n" + error) 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /source/version_helper.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver'); 2 | const { version, release } = require('sequelize/package.json'); 3 | const { QueryTypes } = require('sequelize'); 4 | 5 | module.exports = { 6 | GetSequelizeVersion: function() { 7 | // In v4 and v5 package.json files, have a property 'release: { branch: 'v5' }' 8 | // but in v6 it has 'release: { branches: ['v6'] }' 9 | var branchVersion = release.branches ? release.branches[0] : release.branch; 10 | // When executing the tests on Github Actions the version it gets from sequelize is from the repository which has a development version '0.0.0-development' 11 | // in that case we fallback to a branch version 12 | return semver.coerce(version === '0.0.0-development' ? branchVersion : version); 13 | }, 14 | GetAdapterVersion: function() { 15 | const pkgVersion = require('../package.json').version; 16 | return semver.coerce(pkgVersion); 17 | }, 18 | IsCockroachVersion21_1Plus: async function(connection) { 19 | const versionRow = await connection.query("SELECT version() AS version", { type: QueryTypes.SELECT }); 20 | const cockroachDBVersion = versionRow[0]["version"] 21 | 22 | return semver.gte(semver.coerce(cockroachDBVersion), "21.1.0") 23 | }, 24 | GetCockroachDBVersionFromEnvConfig: function() { 25 | const crdbVersion = process.env['CRDB_VERSION'] 26 | return semver.coerce(crdbVersion) 27 | }, 28 | GetVersionSeries: function(versionStr) { 29 | // Get the version series from a version string. 30 | // E.g. "6.0.1" is of series "6.0". 31 | const regExp=/(\d+\.\d+)(\.|$)/mg 32 | let match = regExp.exec(versionStr); 33 | if (match === null || match.length < 3) { 34 | return null 35 | } 36 | return match[1] 37 | } 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /tests/basic_usage_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | require('./helper'); 16 | 17 | const { expect } = require('chai'); 18 | const { 19 | Sequelize, 20 | Op, 21 | DataTypes, 22 | UniqueConstraintError 23 | } = require('../source'); 24 | const sinon = require('sinon'); 25 | 26 | const wait = ms => new Promise(r => setTimeout(r, ms)); 27 | 28 | function deepToJson(value) { 29 | if (typeof value.toJSON === 'function') return value.toJSON(); 30 | if (Array.isArray(value)) return value.map(x => deepToJson(x)); 31 | return value; 32 | } 33 | 34 | function assertToJsonDeepEquality(actual, expected) { 35 | actual = deepToJson(actual); 36 | expected = deepToJson(expected); 37 | expect(deepToJson(actual)).to.be.deep.equal(deepToJson(expected)); 38 | } 39 | 40 | describe('basic usage', function () { 41 | it('supports CockroachDB', function () { 42 | expect(Sequelize.supportsCockroachDB).to.be.true; 43 | }); 44 | 45 | it('works', async function () { 46 | const Foo = this.sequelize.define('foo', { 47 | name1: { 48 | type: DataTypes.TEXT, 49 | allowNull: false 50 | }, 51 | name2: { 52 | type: DataTypes.STRING, 53 | unique: true 54 | } 55 | }); 56 | const Bar = this.sequelize.define('bar', { 57 | name: DataTypes.STRING(10), 58 | date: DataTypes.DATE 59 | }); 60 | const Baz = this.sequelize.define('baz', { 61 | id: { 62 | type: DataTypes.UUID, 63 | defaultValue: Sequelize.UUIDV4, 64 | primaryKey: true, 65 | autoIncrement: false, 66 | allowNull: false 67 | }, 68 | name: DataTypes.TEXT 69 | }); 70 | const Qux = this.sequelize.define('qux', { age: DataTypes.INTEGER }); 71 | 72 | Foo.hasMany(Bar); 73 | Bar.belongsTo(Foo); 74 | 75 | Bar.belongsToMany(Baz, { through: 'BarBaz' }); 76 | Baz.belongsToMany(Bar, { through: 'BarBaz' }); 77 | 78 | Foo.hasOne(Qux); 79 | Qux.belongsTo(Foo); 80 | 81 | const spy = sinon.spy(); 82 | this.sequelize.afterBulkSync(() => spy()); 83 | await this.sequelize.sync(); 84 | expect(spy).to.have.been.called; 85 | 86 | const foo1 = await Foo.create({ name1: 'foo1', name2: 'foo2' }); 87 | expect(foo1.id).to.be.ok; 88 | expect(foo1.isNewRecord).to.be.false; 89 | expect(foo1.get('name1')).to.equal('foo1'); 90 | expect(foo1.name2).to.equal('foo2'); 91 | 92 | expect(await Foo.count()).to.equal(1); 93 | 94 | await expect( 95 | Foo.create({ name1: 'foo1', name2: 'foo2' }) 96 | ).to.be.eventually.rejectedWith(UniqueConstraintError); 97 | 98 | await expect( 99 | Bar.create({ name: 'this name does not fit' }) 100 | ).to.be.eventually.rejectedWith(/too long/i); 101 | 102 | await expect( 103 | Bar.create({ name: 'bar0', fooId: 1234 }) 104 | ).to.be.eventually.rejectedWith(/foreign key constraint/i); 105 | 106 | const bar1 = await Bar.create({ name: 'bar1', fooId: foo1.id }); 107 | expect(bar1.id).to.be.ok; 108 | expect(bar1.isNewRecord).to.be.false; 109 | 110 | assertToJsonDeepEquality(await bar1.getFoo(), foo1); 111 | assertToJsonDeepEquality(await foo1.getBars(), [bar1]); 112 | assertToJsonDeepEquality(await Foo.findAll({ include: Bar }), [ 113 | { ...foo1.toJSON(), bars: [bar1.toJSON()] } 114 | ]); 115 | 116 | const baz1 = await Baz.create({ name: 'baz1' }); 117 | expect(typeof baz1.id).to.equal('string'); 118 | 119 | const baz2 = await Baz.create({ name: null }); 120 | expect(typeof baz2.id).to.equal('string'); 121 | 122 | await bar1.addBaz(baz1); 123 | await baz2.addBar(bar1); 124 | 125 | expect((await bar1.getBazs()).map(baz => baz.id).sort()).to.be.deep.equal( 126 | [baz1, baz2].map(baz => baz.id).sort() 127 | ); 128 | 129 | expect( 130 | this.sequelize.transaction(async transaction => { 131 | await baz1.update({ name: 'updated' }, { transaction }); 132 | expect(baz1.name).to.equal('updated'); 133 | throw new Error('test12345'); 134 | }) 135 | ).to.be.eventually.rejectedWith('test12345'); 136 | 137 | expect(baz1.name).to.equal('baz1'); 138 | await baz1.reload(); 139 | expect(baz1.name).to.equal('baz1'); 140 | expect((await Baz.findByPk(baz1.id)).name).to.equal('baz1'); 141 | 142 | // Concurrent transactions 143 | const t1 = await this.sequelize.transaction(); 144 | await Qux.create({ age: 10 }, { transaction: t1 }); 145 | const t2 = await this.sequelize.transaction(); 146 | await wait(500); 147 | await Qux.create({ age: 20 }, { transaction: t2 }); 148 | const t3 = await this.sequelize.transaction(); 149 | await Qux.create({ age: 40 }, { transaction: t3 }); 150 | await wait(500); 151 | await Qux.create({ age: 80 }, { transaction: t2 }); 152 | const t4 = await this.sequelize.transaction(); 153 | const t5 = await this.sequelize.transaction(); 154 | await t4.commit(); 155 | await wait(500); 156 | const t6 = await this.sequelize.transaction(); 157 | await wait(500); 158 | await t1.commit(); 159 | const t7 = await this.sequelize.transaction(); 160 | await Qux.create({ age: 160 }, { transaction: t7 }); 161 | await Qux.create({ age: 320 }, { transaction: t3 }); 162 | await t3.commit(); 163 | await t2.rollback(); 164 | await t5.commit(); 165 | await t6.rollback(); 166 | await t7.commit(); 167 | 168 | expect(await Qux.sum('age')).to.equal(530); 169 | 170 | expect(await Qux.sum('age', { where: { age: { [Op.gt]: 30 } } })).to.equal( 171 | 520 172 | ); 173 | 174 | expect( 175 | await Qux.count({ where: { age: { [Op.notIn]: [40, 160] } } }) 176 | ).to.equal(2); 177 | 178 | expect( 179 | await Qux.sum('age', { 180 | where: this.sequelize.where( 181 | this.sequelize.fn('mod', this.sequelize.col('age'), 9), 182 | Op.notIn, 183 | this.sequelize.literal('(4, 5)') 184 | ) 185 | }) 186 | ).to.equal(170); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /tests/belongs_to_test.js: -------------------------------------------------------------------------------- 1 | require('./helper'); 2 | 3 | var expect = require('chai').expect; 4 | 5 | describe('BelongsTo', () => { 6 | describe('foreign key', () => { 7 | // Edit Reason: not a bug, it's how the test is implemented. 8 | // CRDB does not guarantee gapless sequential ids. 9 | it('should set foreignKey on foreign table', async function () { 10 | const Mail = this.sequelize.define('mail', {}, { timestamps: false }); 11 | const Entry = this.sequelize.define('entry', {}, { timestamps: false }); 12 | const User = this.sequelize.define('user', {}, { timestamps: false }); 13 | 14 | Entry.belongsTo(User, { 15 | as: 'owner', 16 | foreignKey: { 17 | name: 'ownerId', 18 | allowNull: false 19 | } 20 | }); 21 | Entry.belongsTo(Mail, { 22 | as: 'mail', 23 | foreignKey: { 24 | name: 'mailId', 25 | allowNull: false 26 | } 27 | }); 28 | Mail.belongsToMany(User, { 29 | as: 'recipients', 30 | through: 'MailRecipients', 31 | otherKey: { 32 | name: 'recipientId', 33 | allowNull: false 34 | }, 35 | foreignKey: { 36 | name: 'mailId', 37 | allowNull: false 38 | }, 39 | timestamps: false 40 | }); 41 | Mail.hasMany(Entry, { 42 | as: 'entries', 43 | foreignKey: { 44 | name: 'mailId', 45 | allowNull: false 46 | } 47 | }); 48 | User.hasMany(Entry, { 49 | as: 'entries', 50 | foreignKey: { 51 | name: 'ownerId', 52 | allowNull: false 53 | } 54 | }); 55 | 56 | await this.sequelize.sync({ force: true }); 57 | const user = await User.create({}); 58 | const mail = await Mail.create({}); 59 | 60 | await Entry.create({ mailId: mail.id, ownerId: user.id }); 61 | await Entry.create({ mailId: mail.id, ownerId: user.id }); 62 | await mail.setRecipients([user.id]); 63 | 64 | const result = await Entry.findAndCountAll({ 65 | offset: 0, 66 | limit: 10, 67 | order: [['id', 'DESC']], 68 | include: [ 69 | { 70 | association: Entry.associations.mail, 71 | include: [ 72 | { 73 | association: Mail.associations.recipients, 74 | through: { 75 | where: { 76 | recipientId: user.id 77 | } 78 | }, 79 | required: true 80 | } 81 | ], 82 | required: true 83 | } 84 | ] 85 | }); 86 | 87 | const rowResult = result.rows[0].get({ plain: true }); 88 | const mailResult = rowResult.mail.recipients[0].MailRecipients; 89 | 90 | expect(result.count).to.equal(2); 91 | expect(rowResult.ownerId).to.equal(user.id); 92 | expect(rowResult.mailId).to.equal(mail.id); 93 | expect(mailResult.mailId).to.equal(mail.id); 94 | expect(mailResult.recipientId).to.equal(user.id); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /tests/change_column_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { expect } = require('chai'); 4 | const { DataTypes } = require('../source'); 5 | const dialect = 'postgres'; 6 | 7 | const Support = { 8 | dropTestSchemas: async sequelize => { 9 | const schemas = await sequelize.showAllSchemas(); 10 | const schemasPromise = []; 11 | schemas.forEach(schema => { 12 | const schemaName = schema.name ? schema.name : schema; 13 | if (schemaName !== sequelize.config.database) { 14 | schemasPromise.push(sequelize.dropSchema(schemaName)); 15 | } 16 | }); 17 | 18 | await Promise.all(schemasPromise.map(p => p.catch(e => e))); 19 | } 20 | }; 21 | 22 | describe('QueryInterface', () => { 23 | beforeEach(function () { 24 | this.sequelize.options.quoteIdenifiers = true; 25 | this.queryInterface = this.sequelize.getQueryInterface(); 26 | }); 27 | 28 | afterEach(async function () { 29 | await Support.dropTestSchemas(this.sequelize); 30 | }); 31 | 32 | describe('changeColumn', () => { 33 | // Reason: ALTER COLUMN TYPE from int to float is prohibited until v21.1 34 | it.skip('should support schemas', async function () { 35 | await this.sequelize.createSchema('archive'); 36 | 37 | await this.queryInterface.createTable( 38 | { 39 | tableName: 'users', 40 | schema: 'archive' 41 | }, 42 | { 43 | id: { 44 | type: DataTypes.INTEGER, 45 | primaryKey: true, 46 | autoIncrement: true 47 | }, 48 | currency: DataTypes.INTEGER 49 | } 50 | ); 51 | 52 | await this.queryInterface.changeColumn( 53 | { 54 | tableName: 'users', 55 | schema: 'archive' 56 | }, 57 | 'currency', 58 | { 59 | type: DataTypes.FLOAT 60 | } 61 | ); 62 | 63 | const table = await this.queryInterface.describeTable({ 64 | tableName: 'users', 65 | schema: 'archive' 66 | }); 67 | 68 | if (dialect === 'postgres' || dialect === 'postgres-native') { 69 | expect(table.currency.type).to.equal('DOUBLE PRECISION'); 70 | } else { 71 | expect(table.currency.type).to.equal('FLOAT'); 72 | } 73 | }); 74 | 75 | // Reason: ALTER COLUMN TYPE from int to float is prohibited until v21.1 76 | it.skip('should change columns', async function () { 77 | await this.queryInterface.createTable( 78 | { 79 | tableName: 'users' 80 | }, 81 | { 82 | id: { 83 | type: DataTypes.INTEGER, 84 | primaryKey: true, 85 | autoIncrement: true 86 | }, 87 | currency: DataTypes.INTEGER 88 | } 89 | ); 90 | 91 | await this.queryInterface.changeColumn('users', 'currency', { 92 | type: DataTypes.FLOAT, 93 | allowNull: true 94 | }); 95 | 96 | const table = await this.queryInterface.describeTable({ 97 | tableName: 'users' 98 | }); 99 | 100 | if (dialect === 'postgres' || dialect === 'postgres-native') { 101 | expect(table.currency.type).to.equal('DOUBLE PRECISION'); 102 | } else { 103 | expect(table.currency.type).to.equal('FLOAT'); 104 | } 105 | }); 106 | 107 | //Reason: ALTER COLUMN TYPE from varchar to enum_users_firstName is prohibited until v21.1 108 | it.skip('should work with enums (case 1)', async function () { 109 | await this.queryInterface.createTable( 110 | { 111 | tableName: 'users' 112 | }, 113 | { 114 | firstName: DataTypes.STRING 115 | } 116 | ); 117 | 118 | await this.queryInterface.changeColumn('users', 'firstName', { 119 | type: DataTypes.ENUM(['value1', 'value2', 'value3']) 120 | }); 121 | }); 122 | 123 | // Reason: ALTER COLUMN TYPE from varchar to enum_users_firstName is prohibited until v21.1 124 | it.skip('should work with enums (case 2)', async function () { 125 | await this.queryInterface.createTable( 126 | { 127 | tableName: 'users' 128 | }, 129 | { 130 | firstName: DataTypes.STRING 131 | } 132 | ); 133 | 134 | await this.queryInterface.changeColumn('users', 'firstName', { 135 | type: DataTypes.ENUM, 136 | values: ['value1', 'value2', 'value3'] 137 | }); 138 | }); 139 | 140 | // Reason: ALTER COLUMN TYPE from varchar to enum_users_firstName is prohibited until v21.1 141 | it.skip('should work with enums with schemas', async function () { 142 | await this.sequelize.createSchema('archive'); 143 | 144 | await this.queryInterface.createTable( 145 | { 146 | tableName: 'users', 147 | schema: 'archive' 148 | }, 149 | { 150 | firstName: DataTypes.STRING 151 | } 152 | ); 153 | 154 | await this.queryInterface.changeColumn( 155 | { 156 | tableName: 'users', 157 | schema: 'archive' 158 | }, 159 | 'firstName', 160 | { 161 | type: DataTypes.ENUM(['value1', 'value2', 'value3']) 162 | } 163 | ); 164 | }); 165 | 166 | describe('should support foreign keys', () => { 167 | beforeEach(async function () { 168 | await this.queryInterface.createTable('users', { 169 | id: { 170 | type: DataTypes.INTEGER, 171 | primaryKey: true, 172 | autoIncrement: true 173 | }, 174 | level_id: { 175 | type: DataTypes.INTEGER, 176 | allowNull: false 177 | } 178 | }); 179 | 180 | await this.queryInterface.createTable('level', { 181 | id: { 182 | type: DataTypes.INTEGER, 183 | primaryKey: true, 184 | autoIncrement: true 185 | } 186 | }); 187 | }); 188 | 189 | it('able to change column to foreign key', async function () { 190 | const foreignKeys = await this.queryInterface.getForeignKeyReferencesForTable( 191 | 'users' 192 | ); 193 | expect(foreignKeys).to.be.an('array'); 194 | expect(foreignKeys).to.be.empty; 195 | 196 | await this.queryInterface.changeColumn('users', 'level_id', { 197 | type: DataTypes.INTEGER, 198 | references: { 199 | model: 'level', 200 | key: 'id' 201 | }, 202 | onUpdate: 'cascade', 203 | onDelete: 'cascade' 204 | }); 205 | 206 | const newForeignKeys = await this.queryInterface.getForeignKeyReferencesForTable( 207 | 'users' 208 | ); 209 | expect(newForeignKeys).to.be.an('array'); 210 | expect(newForeignKeys).to.have.lengthOf(1); 211 | expect(newForeignKeys[0].columnName).to.be.equal('level_id'); 212 | }); 213 | 214 | it('able to change column property without affecting other properties', async function () { 215 | // 1. look for users table information 216 | // 2. change column level_id on users to have a Foreign Key 217 | // 3. look for users table Foreign Keys information 218 | // 4. change column level_id AGAIN to allow null values 219 | // 5. look for new foreign keys information 220 | // 6. look for new table structure information 221 | // 7. compare foreign keys and tables(before and after the changes) 222 | const firstTable = await this.queryInterface.describeTable({ 223 | tableName: 'users' 224 | }); 225 | 226 | await this.queryInterface.changeColumn('users', 'level_id', { 227 | type: DataTypes.INTEGER, 228 | references: { 229 | model: 'level', 230 | key: 'id' 231 | }, 232 | onUpdate: 'cascade', 233 | onDelete: 'cascade' 234 | }); 235 | 236 | const keys = await this.queryInterface.getForeignKeyReferencesForTable( 237 | 'users' 238 | ); 239 | const firstForeignKeys = keys; 240 | 241 | await this.queryInterface.changeColumn('users', 'level_id', { 242 | type: DataTypes.INTEGER, 243 | allowNull: true 244 | }); 245 | 246 | const newForeignKeys = await this.queryInterface.getForeignKeyReferencesForTable( 247 | 'users' 248 | ); 249 | expect(firstForeignKeys.length).to.be.equal(newForeignKeys.length); 250 | expect(firstForeignKeys[0].columnName).to.be.equal('level_id'); 251 | expect(firstForeignKeys[0].columnName).to.be.equal( 252 | newForeignKeys[0].columnName 253 | ); 254 | 255 | const describedTable = await this.queryInterface.describeTable({ 256 | tableName: 'users' 257 | }); 258 | 259 | expect(describedTable.level_id).to.have.property('allowNull'); 260 | expect(describedTable.level_id.allowNull).to.not.equal( 261 | firstTable.level_id.allowNull 262 | ); 263 | expect(describedTable.level_id.allowNull).to.be.equal(true); 264 | }); 265 | 266 | it('should change the comment of column', async function () { 267 | const describedTable = await this.queryInterface.describeTable({ 268 | tableName: 'users' 269 | }); 270 | 271 | expect(describedTable.level_id.comment).to.be.equal(null); 272 | 273 | await this.queryInterface.changeColumn('users', 'level_id', { 274 | type: DataTypes.INTEGER, 275 | comment: 'FooBar' 276 | }); 277 | 278 | const describedTable2 = await this.queryInterface.describeTable({ 279 | tableName: 'users' 280 | }); 281 | expect(describedTable2.level_id.comment).to.be.equal('FooBar'); 282 | }); 283 | }); 284 | }); 285 | }); 286 | -------------------------------------------------------------------------------- /tests/cls_test.js: -------------------------------------------------------------------------------- 1 | require('./helper'); 2 | 3 | var expect = require('chai').expect; 4 | var Sequelize = require('..'); 5 | var cls = require('cls-hooked'); 6 | var delay = require('delay'); 7 | var sinon = require('sinon'); 8 | 9 | describe('CLS (Async hooks)', () => { 10 | before(() => { 11 | Sequelize.useCLS(cls.createNamespace('sequelize')); 12 | }); 13 | 14 | after(() => { 15 | cls.destroyNamespace('sequelize'); 16 | delete Sequelize._cls; 17 | }); 18 | 19 | beforeEach(async function () { 20 | this.ns = cls.getNamespace('sequelize'); 21 | this.User = this.sequelize.define('user', { 22 | name: Sequelize.STRING 23 | }); 24 | await this.sequelize.sync({ force: true }); 25 | }); 26 | 27 | describe('context', () => { 28 | it('does not use continuation storage on manually managed transactions', async function () { 29 | await Sequelize._clsRun(async () => { 30 | const transaction = await this.sequelize.transaction(); 31 | expect(this.ns.get('transaction')).not.to.be.ok; 32 | await transaction.rollback(); 33 | }); 34 | }); 35 | 36 | it('supports several concurrent transactions', async function () { 37 | let t1id, t2id; 38 | await Promise.all([ 39 | this.sequelize.transaction(async () => { 40 | t1id = this.ns.get('transaction').id; 41 | }), 42 | this.sequelize.transaction(async () => { 43 | t2id = this.ns.get('transaction').id; 44 | }) 45 | ]); 46 | expect(t1id).to.be.ok; 47 | expect(t2id).to.be.ok; 48 | expect(t1id).not.to.equal(t2id); 49 | }); 50 | 51 | it('supports nested promise chains', async function () { 52 | await this.sequelize.transaction(async () => { 53 | const tid = this.ns.get('transaction').id; 54 | 55 | await this.User.findAll(); 56 | expect(this.ns.get('transaction').id).to.be.ok; 57 | expect(this.ns.get('transaction').id).to.equal(tid); 58 | }); 59 | }); 60 | 61 | it('does not leak variables to the outer scope', async function () { 62 | // This is a little tricky. We want to check the values in the outer scope, when the transaction has been successfully set up, but before it has been comitted. 63 | // We can't just call another function from inside that transaction, since that would transfer the context to that function - exactly what we are trying to prevent; 64 | 65 | let transactionSetup = false, 66 | transactionEnded = false; 67 | 68 | const clsTask = this.sequelize.transaction(async () => { 69 | transactionSetup = true; 70 | await delay(500); 71 | expect(this.ns.get('transaction')).to.be.ok; 72 | transactionEnded = true; 73 | }); 74 | 75 | await new Promise(resolve => { 76 | // Wait for the transaction to be setup 77 | const interval = setInterval(() => { 78 | if (transactionSetup) { 79 | clearInterval(interval); 80 | resolve(); 81 | } 82 | }, 200); 83 | }); 84 | expect(transactionEnded).not.to.be.ok; 85 | 86 | expect(this.ns.get('transaction')).not.to.be.ok; 87 | 88 | // Just to make sure it didn't change between our last check and the assertion 89 | expect(transactionEnded).not.to.be.ok; 90 | await clsTask; // ensure we don't leak the promise 91 | }); 92 | 93 | it('does not leak variables to the following promise chain', async function () { 94 | await this.sequelize.transaction(() => {}); 95 | expect(this.ns.get('transaction')).not.to.be.ok; 96 | }); 97 | 98 | it('does not leak outside findOrCreate', async function () { 99 | await this.User.findOrCreate({ 100 | where: { 101 | name: 'Kafka' 102 | }, 103 | logging(sql) { 104 | if (/default/.test(sql)) { 105 | throw new Error('The transaction was not properly assigned'); 106 | } 107 | } 108 | }); 109 | 110 | await this.User.findAll(); 111 | }); 112 | }); 113 | 114 | // Reason: https://github.com/cockroachdb/cockroach/issues/61269 115 | describe.skip('sequelize.query integration', () => { 116 | it('automagically uses the transaction in all calls', async function () { 117 | await this.sequelize.transaction(async () => { 118 | await this.User.create({ name: 'bob' }); 119 | return Promise.all([ 120 | expect( 121 | this.User.findAll({ transaction: null }) 122 | ).to.eventually.have.length(0), 123 | expect(this.User.findAll({})).to.eventually.have.length(1) 124 | ]); 125 | }); 126 | }); 127 | 128 | it('automagically uses the transaction in all calls with async/await', async function () { 129 | await this.sequelize.transaction(async () => { 130 | await this.User.create({ name: 'bob' }); 131 | expect(await this.User.findAll({ transaction: null })).to.have.length( 132 | 0 133 | ); 134 | expect(await this.User.findAll({})).to.have.length(1); 135 | }); 136 | }); 137 | }); 138 | 139 | it('CLS namespace is stored in Sequelize._cls', function () { 140 | expect(Sequelize._cls).to.equal(this.ns); 141 | }); 142 | 143 | it('promises returned by sequelize.query are correctly patched', async function () { 144 | await this.sequelize.transaction(async t => { 145 | await this.sequelize.query('select 1', { 146 | type: Sequelize.QueryTypes.SELECT 147 | }); 148 | return expect(this.ns.get('transaction')).to.equal(t); 149 | }); 150 | }); 151 | 152 | // Reason: would need to implement Support which is a test configuration file 153 | it.skip('custom logging with benchmarking has correct CLS context', async function () { 154 | const Support = {}; 155 | const logger = sinon.spy(() => { 156 | return this.ns.get('value'); 157 | }); 158 | const sequelize = Support.createSequelizeInstance({ 159 | logging: logger, 160 | benchmark: true 161 | }); 162 | 163 | const result = this.ns.runPromise(async () => { 164 | this.ns.set('value', 1); 165 | await delay(500); 166 | return sequelize.query('select 1;'); 167 | }); 168 | 169 | await this.ns.runPromise(() => { 170 | this.ns.set('value', 2); 171 | return sequelize.query('select 2;'); 172 | }); 173 | 174 | await result; 175 | 176 | expect(logger.calledTwice).to.be.true; 177 | expect(logger.firstCall.args[0]).to.be.match( 178 | /Executed \((\d*|default)\): select 2/ 179 | ); 180 | expect(logger.firstCall.returnValue).to.be.equal(2); 181 | expect(logger.secondCall.args[0]).to.be.match( 182 | /Executed \((\d*|default)\): select 1/ 183 | ); 184 | expect(logger.secondCall.returnValue).to.be.equal(1); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /tests/connection_manager_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const { expect } = require('chai'), 6 | DataTypes = require('../source'); 7 | 8 | describe('Dynamic OIDs', () => { 9 | // Edit Reason: 10 | // CRDB does not support HSTORE and CITEXT. 11 | const dynamicTypesToCheck = [ 12 | DataTypes.GEOMETRY, 13 | // DataTypes.HSTORE, 14 | DataTypes.GEOGRAPHY 15 | // DataTypes.CITEXT 16 | ]; 17 | 18 | // Expect at least these 19 | const expCastTypes = { 20 | integer: 'int4', 21 | decimal: 'numeric', 22 | date: 'timestamptz', 23 | dateonly: 'date', 24 | bigint: 'int8' 25 | }; 26 | 27 | function reloadDynamicOIDs(sequelize) { 28 | // Reset oids so we need to refetch them 29 | sequelize.connectionManager._clearDynamicOIDs(); 30 | sequelize.connectionManager._clearTypeParser(); 31 | 32 | // Force start of connection manager to reload dynamic OIDs 33 | const User = sequelize.define('User', { 34 | perms: DataTypes.ENUM(['foo', 'bar']) 35 | }); 36 | 37 | return User.sync({ force: true }); 38 | } 39 | 40 | // Skip on CI: 41 | // Edited dynamicTypesToCheck 42 | it('should fetch regular dynamic oids and create parsers', async function () { 43 | const sequelize = this.sequelize; 44 | await reloadDynamicOIDs(sequelize); 45 | dynamicTypesToCheck.forEach(type => { 46 | expect(type.types.postgres, `DataType.${type.key}.types.postgres`).to.not 47 | .be.empty; 48 | 49 | for (const name of type.types.postgres) { 50 | const entry = sequelize.connectionManager.nameOidMap[name]; 51 | const oidParserMap = sequelize.connectionManager.oidParserMap; 52 | expect(entry.oid, `nameOidMap[${name}].oid`).to.be.a('number'); 53 | expect(entry.arrayOid, `nameOidMap[${name}].arrayOid`).to.be.a( 54 | 'number' 55 | ); 56 | 57 | expect( 58 | oidParserMap.get(entry.oid), 59 | `oidParserMap.get(nameOidMap[${name}].oid)` 60 | ).to.be.a('function'); 61 | expect( 62 | oidParserMap.get(entry.arrayOid), 63 | `oidParserMap.get(nameOidMap[${name}].arrayOid)` 64 | ).to.be.a('function'); 65 | } 66 | }); 67 | }); 68 | 69 | // Skip reason: 70 | // CRDB does not have Range types. 71 | it.skip('should fetch range dynamic oids and create parsers', async function () { 72 | const sequelize = this.sequelize; 73 | await reloadDynamicOIDs(sequelize); 74 | for (const baseKey in expCastTypes) { 75 | console.log(baseKey); 76 | const name = expCastTypes[baseKey]; 77 | console.log(name); 78 | const entry = sequelize.connectionManager.nameOidMap[name]; 79 | const oidParserMap = sequelize.connectionManager.oidParserMap; 80 | console.log(entry); 81 | 82 | for (const key of ['rangeOid', 'arrayRangeOid']) { 83 | expect(entry[key], `nameOidMap[${name}][${key}]`).to.be.a('number'); 84 | } 85 | 86 | expect( 87 | oidParserMap.get(entry.rangeOid), 88 | `oidParserMap.get(nameOidMap[${name}].rangeOid)` 89 | ).to.be.a('function'); 90 | expect( 91 | oidParserMap.get(entry.arrayRangeOid), 92 | `oidParserMap.get(nameOidMap[${name}].arrayRangeOid)` 93 | ).to.be.a('function'); 94 | } 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /tests/create-table_test.js: -------------------------------------------------------------------------------- 1 | require('./helper'); 2 | 3 | var expect = require('chai').expect; 4 | var Sequelize = require('..'); 5 | var DataTypes = Sequelize.DataTypes; 6 | const dialect = 'postgres'; 7 | 8 | const semver = require('semver'); 9 | const version_helper = require('../source/version_helper.js') 10 | const crdbVersion = version_helper.GetCockroachDBVersionFromEnvConfig() 11 | 12 | describe('QueryInterface', () => { 13 | beforeEach(function () { 14 | this.sequelize.options.quoteIdenifiers = true; 15 | this.queryInterface = this.sequelize.getQueryInterface(); 16 | }); 17 | 18 | describe('createTable', () => { 19 | // Reason: expected 'unique_rowid()' to equal 'nextval("TableWithPK_table_id_seq"::regclass)' 20 | // Cockroach nextval expects a string instead of a regclass. 21 | it.skip('should create a auto increment primary key', async function () { 22 | await this.queryInterface.createTable('TableWithPK', { 23 | table_id: { 24 | type: DataTypes.INTEGER, 25 | primaryKey: true, 26 | autoIncrement: true 27 | } 28 | }); 29 | 30 | const result = await this.queryInterface.describeTable('TableWithPK'); 31 | 32 | if (dialect === 'mssql' || dialect === 'mysql' || dialect === 'mariadb') { 33 | expect(result.table_id.autoIncrement).to.be.true; 34 | } else if (dialect === 'postgres') { 35 | expect(result.table_id.defaultValue).to.equal( 36 | 'nextval("TableWithPK_table_id_seq"::regclass)' 37 | ); 38 | } 39 | }); 40 | 41 | it('should create unique constraint with uniqueKeys', async function () { 42 | await this.queryInterface.createTable( 43 | 'MyTable', 44 | { 45 | id: { 46 | type: DataTypes.INTEGER, 47 | primaryKey: true, 48 | autoIncrement: true 49 | }, 50 | name: { 51 | type: DataTypes.STRING 52 | }, 53 | email: { 54 | type: DataTypes.STRING 55 | } 56 | }, 57 | { 58 | uniqueKeys: { 59 | myCustomIndex: { 60 | fields: ['name', 'email'] 61 | }, 62 | myOtherIndex: { 63 | fields: ['name'] 64 | } 65 | } 66 | } 67 | ); 68 | 69 | const indexes = await this.queryInterface.showIndex('MyTable'); 70 | switch (dialect) { 71 | case 'postgres': 72 | case 'postgres-native': 73 | case 'sqlite': 74 | case 'mssql': 75 | // name + email 76 | expect(indexes[0].unique).to.be.true; 77 | expect(indexes[0].fields[0].attribute).to.equal('name'); 78 | expect(indexes[0].fields[1].attribute).to.equal('email'); 79 | 80 | // name 81 | expect(indexes[1].unique).to.be.true; 82 | expect(indexes[1].fields[0].attribute).to.equal('name'); 83 | break; 84 | case 'mariadb': 85 | case 'mysql': 86 | // name + email 87 | expect(indexes[1].unique).to.be.true; 88 | expect(indexes[1].fields[0].attribute).to.equal('name'); 89 | expect(indexes[1].fields[1].attribute).to.equal('email'); 90 | 91 | // name 92 | expect(indexes[2].unique).to.be.true; 93 | expect(indexes[2].fields[0].attribute).to.equal('name'); 94 | break; 95 | default: 96 | throw new Error(`Not implemented fpr ${dialect}`); 97 | } 98 | }); 99 | 100 | it('should work with schemas', async function () { 101 | await this.sequelize.createSchema('hero'); 102 | 103 | await this.queryInterface.createTable( 104 | 'User', 105 | { 106 | name: { 107 | type: DataTypes.STRING 108 | } 109 | }, 110 | { 111 | schema: 'hero' 112 | } 113 | ); 114 | }); 115 | 116 | describe('enums', () => { 117 | it('should work with enums (1)', async function () { 118 | await this.queryInterface.createTable('SomeTable', { 119 | someEnum: DataTypes.ENUM('value1', 'value2', 'value3') 120 | }); 121 | 122 | const table = await this.queryInterface.describeTable('SomeTable'); 123 | expect(table.someEnum.special).to.deep.equal([ 124 | 'value1', 125 | 'value2', 126 | 'value3' 127 | ]); 128 | }); 129 | 130 | it('should work with enums (2)', async function () { 131 | await this.queryInterface.createTable('SomeTable', { 132 | someEnum: { 133 | type: DataTypes.ENUM, 134 | values: ['value1', 'value2', 'value3'] 135 | } 136 | }); 137 | 138 | const table = await this.queryInterface.describeTable('SomeTable'); 139 | if (dialect.includes('postgres')) { 140 | expect(table.someEnum.special).to.deep.equal([ 141 | 'value1', 142 | 'value2', 143 | 'value3' 144 | ]); 145 | } 146 | }); 147 | 148 | it('should work with enums (3)', async function () { 149 | await this.queryInterface.createTable('SomeTable', { 150 | someEnum: { 151 | type: DataTypes.ENUM, 152 | values: ['value1', 'value2', 'value3'], 153 | field: 'otherName' 154 | } 155 | }); 156 | 157 | const table = await this.queryInterface.describeTable('SomeTable'); 158 | if (dialect.includes('postgres')) { 159 | expect(table.otherName.special).to.deep.equal([ 160 | 'value1', 161 | 'value2', 162 | 'value3' 163 | ]); 164 | } 165 | }); 166 | 167 | it('should work with enums (4)', async function () { 168 | await this.queryInterface.createSchema('archive'); 169 | 170 | await this.queryInterface.createTable( 171 | 'SomeTable', 172 | { 173 | someEnum: { 174 | type: DataTypes.ENUM, 175 | values: ['value1', 'value2', 'value3'], 176 | field: 'otherName' 177 | } 178 | }, 179 | { schema: 'archive' } 180 | ); 181 | 182 | const table = await this.queryInterface.describeTable('SomeTable', { 183 | schema: 'archive' 184 | }); 185 | if (dialect.includes('postgres')) { 186 | expect(table.otherName.special).to.deep.equal([ 187 | 'value1', 188 | 'value2', 189 | 'value3' 190 | ]); 191 | } 192 | }); 193 | 194 | it('should work with enums (5)', async function () { 195 | await this.queryInterface.createTable('SomeTable', { 196 | someEnum: { 197 | type: DataTypes.ENUM(['COMMENT']), 198 | comment: 'special enum col' 199 | } 200 | }); 201 | 202 | const table = await this.queryInterface.describeTable('SomeTable'); 203 | if (dialect.includes('postgres')) { 204 | expect(table.someEnum.special).to.deep.equal(['COMMENT']); 205 | expect(table.someEnum.comment).to.equal('special enum col'); 206 | } 207 | }); 208 | }); 209 | }); 210 | }); 211 | -------------------------------------------------------------------------------- /tests/dao_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Skip reason: This test implements both HSTORE and an array of JSONB. 4 | // CRDB does not support these. 5 | describe.skip('[POSTGRES Specific] DAO', () => { 6 | }); 7 | -------------------------------------------------------------------------------- /tests/data_types_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const { expect } = require('chai'), 6 | { Sequelize, DataTypes } = require('../source'), 7 | sinon = require('sinon'), 8 | _ = require('lodash'), 9 | Op = Sequelize.Op, 10 | dialect = 'postgres'; 11 | 12 | describe('DataTypes', function () { 13 | afterEach(function () { 14 | // Restore some sanity by resetting all parsers 15 | this.sequelize.connectionManager._clearTypeParser(); 16 | this.sequelize.connectionManager.refreshTypeParser(DataTypes[dialect]); // Reload custom parsers 17 | }); 18 | 19 | const testSuccess = async function (sequelize, Type, value, options) { 20 | const parse = (Type.constructor.parse = sinon.spy(value => { 21 | return value; 22 | })); 23 | 24 | const stringify = (Type.constructor.prototype.stringify = sinon.spy( 25 | function () { 26 | return Sequelize.ABSTRACT.prototype.stringify.apply(this, arguments); 27 | } 28 | )); 29 | let bindParam; 30 | if (options && options.useBindParam) { 31 | bindParam = Type.constructor.prototype.bindParam = sinon.spy(function () { 32 | return Sequelize.ABSTRACT.prototype.bindParam.apply(this, arguments); 33 | }); 34 | } 35 | 36 | const User = sequelize.define( 37 | 'user', 38 | { 39 | field: Type 40 | }, 41 | { 42 | timestamps: false 43 | } 44 | ); 45 | 46 | await sequelize.sync({ force: true }); 47 | 48 | sequelize.refreshTypes(); 49 | 50 | await User.create({ 51 | field: value 52 | }); 53 | 54 | await User.findAll(); 55 | 56 | expect(parse).to.have.been.called; 57 | if (options && options.useBindParam) { 58 | expect(bindParam).to.have.been.called; 59 | } else { 60 | expect(stringify).to.have.been.called; 61 | } 62 | 63 | delete Type.constructor.parse; 64 | delete Type.constructor.prototype.stringify; 65 | if (options && options.useBindParam) { 66 | delete Type.constructor.prototype.bindParam; 67 | } 68 | }; 69 | 70 | // Skip reason: In CRDB, JSON is an alias for JSONB. Although calling testSuccess with JSON 71 | // It'll return the data and parse it as the JSONB it is. 72 | // https://www.cockroachlabs.com/docs/v20.2/jsonb.html 73 | it.skip('calls parse and stringify for JSON', async function () { 74 | const Type = new Sequelize.JSON(); 75 | 76 | await testSuccess(this.sequelize, Type, { 77 | test: 42, 78 | nested: { foo: 'bar' } 79 | }); 80 | }); 81 | 82 | // Skip reason: CRDB does not support HSTORE Type. 83 | // https://www.cockroachlabs.com/docs/v20.2/data-types.html 84 | it.skip('calls parse and bindParam for HSTORE', async function () { 85 | const Type = new Sequelize.HSTORE(); 86 | 87 | await testSuccess( 88 | this.sequelize, 89 | Type, 90 | { test: 42, nested: false }, 91 | { useBindParam: true } 92 | ); 93 | }); 94 | 95 | // Skip reason: CRDB does not support RANGE Type. 96 | // https://www.cockroachlabs.com/docs/v20.2/data-types.html 97 | it.skip('calls parse and bindParam for RANGE', async function () { 98 | const Type = new Sequelize.RANGE(new Sequelize.INTEGER()); 99 | 100 | await testSuccess(this.sequelize, Type, [1, 2], { useBindParam: true }); 101 | }); 102 | 103 | // Skip reason: CRDB uses 8 byte INTEGER instead of PG 4 byte. 104 | // CRDB: https://www.cockroachlabs.com/docs/v20.2/int 105 | // PG: https://www.postgresql.org/docs/9.5/datatype-numeric.html 106 | // To treat it Sequelize-friendly, Type should be Sequelize.BIGINT() 107 | // instead of Sequelize.INTEGER(). 108 | it.skip('calls parse and stringify for INTEGER', async function () { 109 | const Type = new Sequelize.INTEGER(); 110 | 111 | await testSuccess(this.sequelize, Type, 1); 112 | }); 113 | 114 | // Skip reason: CRDB does not support CIDR Type. 115 | // https://www.cockroachlabs.com/docs/v20.2/data-types.html 116 | it.skip('calls parse and stringify for CIDR', async function () { 117 | const Type = new Sequelize.CIDR(); 118 | 119 | await testSuccess(this.sequelize, Type, '10.1.2.3/32'); 120 | }); 121 | 122 | // Skip reason: CRDB does not support CITEXT Type. 123 | // https://www.cockroachlabs.com/docs/v20.2/data-types.html 124 | it.skip('calls parse and stringify for CITEXT', async function () { 125 | const Type = new Sequelize.CITEXT(); 126 | 127 | await testSuccess(this.sequelize, Type, 'foobar'); 128 | }); 129 | 130 | // Skip reason: CRDB does not support MACADDR Type. 131 | // https://www.cockroachlabs.com/docs/v20.2/data-types.html 132 | it.skip('calls parse and stringify for MACADDR', async function () { 133 | const Type = new Sequelize.MACADDR(); 134 | 135 | await testSuccess(this.sequelize, Type, '01:23:45:67:89:ab'); 136 | }); 137 | 138 | // Skip reason: CRDB does not support TSVECTOR Type. 139 | // https://www.cockroachlabs.com/docs/v20.2/data-types.html 140 | it.skip('calls parse and stringify for TSVECTOR', async function () { 141 | const Type = new Sequelize.TSVECTOR(); 142 | 143 | await testSuccess(this.sequelize, Type, 'swagger'); 144 | }); 145 | 146 | // TODO get back at it when possible 147 | // Fail reason: SEQUELIZE for some reason is not validating Infinity as a Float or DOUBLE. 148 | // https://github.com/sequelize/sequelize/blob/main/lib/data-types.js#L209 149 | // throws: SequelizeValidationError: null is not a valid double precision 150 | // Issue: https://github.com/sequelize/sequelize/issues/13274 151 | it.skip('should store and parse IEEE floating point literals (NaN and Infinity)', async function () { 152 | const Model = this.sequelize.define('model', { 153 | float: Sequelize.FLOAT, 154 | double: Sequelize.DOUBLE, 155 | real: Sequelize.REAL 156 | }); 157 | 158 | await Model.sync({ force: true }); 159 | 160 | const r = await Model.create({ 161 | id: 1, 162 | float: NaN, 163 | double: Infinity, 164 | real: -Infinity 165 | }); 166 | 167 | const user = await Model.findOne({ where: { id: 1 } }); 168 | expect(user.get('float')).to.be.NaN; 169 | expect(user.get('double')).to.eq(Infinity); 170 | expect(user.get('real')).to.eq(-Infinity); 171 | }); 172 | 173 | // Skip reason: CRDB does not support RANGE. 174 | // https://www.cockroachlabs.com/docs/v20.2/data-types.html 175 | it.skip('should return Int4 range properly #5747', async function () { 176 | const Model = this.sequelize.define('M', { 177 | interval: { 178 | type: Sequelize.RANGE(Sequelize.INTEGER), 179 | allowNull: false, 180 | unique: true 181 | } 182 | }); 183 | 184 | await Model.sync({ force: true }); 185 | await Model.create({ interval: [1, 4] }); 186 | const [m] = await Model.findAll(); 187 | expect(m.interval[0].value).to.be.eql(1); 188 | expect(m.interval[1].value).to.be.eql(4); 189 | }); 190 | 191 | // Skip reason: All tests below expect RANGE Type. CRDB does not support it. 192 | // https://www.cockroachlabs.com/docs/v20.2/data-types.html 193 | // if (current.dialect.supports.RANGE) { 194 | it.skip('should allow date ranges to be generated with default bounds inclusion #8176', async function () { 195 | const Model = this.sequelize.define('M', { 196 | interval: { 197 | type: Sequelize.RANGE(Sequelize.DATE), 198 | allowNull: false, 199 | unique: true 200 | } 201 | }); 202 | const testDate1 = new Date(); 203 | const testDate2 = new Date(testDate1.getTime() + 10000); 204 | const testDateRange = [testDate1, testDate2]; 205 | 206 | await Model.sync({ force: true }); 207 | await Model.create({ interval: testDateRange }); 208 | const m = await Model.findOne(); 209 | expect(m).to.exist; 210 | expect(m.interval[0].value).to.be.eql(testDate1); 211 | expect(m.interval[1].value).to.be.eql(testDate2); 212 | expect(m.interval[0].inclusive).to.be.eql(true); 213 | expect(m.interval[1].inclusive).to.be.eql(false); 214 | }); 215 | 216 | it.skip('should allow date ranges to be generated using a single range expression to define bounds inclusion #8176', async function () { 217 | const Model = this.sequelize.define('M', { 218 | interval: { 219 | type: Sequelize.RANGE(Sequelize.DATE), 220 | allowNull: false, 221 | unique: true 222 | } 223 | }); 224 | const testDate1 = new Date(); 225 | const testDate2 = new Date(testDate1.getTime() + 10000); 226 | const testDateRange = [ 227 | { value: testDate1, inclusive: false }, 228 | { value: testDate2, inclusive: true } 229 | ]; 230 | 231 | await Model.sync({ force: true }); 232 | await Model.create({ interval: testDateRange }); 233 | const m = await Model.findOne(); 234 | expect(m).to.exist; 235 | expect(m.interval[0].value).to.be.eql(testDate1); 236 | expect(m.interval[1].value).to.be.eql(testDate2); 237 | expect(m.interval[0].inclusive).to.be.eql(false); 238 | expect(m.interval[1].inclusive).to.be.eql(true); 239 | }); 240 | 241 | it.skip('should allow date ranges to be generated using a composite range expression #8176', async function () { 242 | const Model = this.sequelize.define('M', { 243 | interval: { 244 | type: Sequelize.RANGE(Sequelize.DATE), 245 | allowNull: false, 246 | unique: true 247 | } 248 | }); 249 | const testDate1 = new Date(); 250 | const testDate2 = new Date(testDate1.getTime() + 10000); 251 | const testDateRange = [testDate1, { value: testDate2, inclusive: true }]; 252 | 253 | await Model.sync({ force: true }); 254 | await Model.create({ interval: testDateRange }); 255 | const m = await Model.findOne(); 256 | expect(m).to.exist; 257 | expect(m.interval[0].value).to.be.eql(testDate1); 258 | expect(m.interval[1].value).to.be.eql(testDate2); 259 | expect(m.interval[0].inclusive).to.be.eql(true); 260 | expect(m.interval[1].inclusive).to.be.eql(true); 261 | }); 262 | 263 | it.skip('should correctly return ranges when using predicates that define bounds inclusion #8176', async function () { 264 | const Model = this.sequelize.define('M', { 265 | interval: { 266 | type: Sequelize.RANGE(Sequelize.DATE), 267 | allowNull: false, 268 | unique: true 269 | } 270 | }); 271 | const testDate1 = new Date(); 272 | const testDate2 = new Date(testDate1.getTime() + 10000); 273 | const testDateRange = [testDate1, testDate2]; 274 | const dateRangePredicate = [ 275 | { value: testDate1, inclusive: true }, 276 | { value: testDate1, inclusive: true } 277 | ]; 278 | 279 | await Model.sync({ force: true }); 280 | await Model.create({ interval: testDateRange }); 281 | 282 | const m = await Model.findOne({ 283 | where: { 284 | interval: { [Op.overlap]: dateRangePredicate } 285 | } 286 | }); 287 | 288 | expect(m).to.exist; 289 | }); 290 | 291 | // GEOMETRY is not yet working with this ORM. 292 | // https://github.com/cockroachdb/sequelize-cockroachdb/issues/52 293 | // Error: SequelizeDatabaseError: st_geomfromgeojson(): could not determine data type of placeholder $1. 294 | it.skip('calls parse and bindParam for GEOMETRY', async function () { 295 | const Type = new Sequelize.GEOMETRY(); 296 | 297 | console.log(this.sequelize) 298 | await testSuccess(this.sequelize, Type, { type: 'Point', coordinates: [125.6, 10.1] }, { useBindParam: true }); 299 | }); 300 | }); 301 | -------------------------------------------------------------------------------- /tests/dialects_data_types_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { expect } = require('chai'); 4 | 5 | describe('[POSTGRES Specific] Data Types', () => { 6 | describe('DATE SQL', () => { 7 | // Skip reason: There is a known issue with CRDB to treat correctly Infinity in DATE Type. 8 | // https://github.com/cockroachdb/cockroach/issues/41564 9 | // create dummy user 10 | it.skip('should be able to create and update records with Infinity/-Infinity', async function () { 11 | this.sequelize.options.typeValidation = true; 12 | 13 | const date = new Date(); 14 | const User = this.sequelize.define( 15 | 'User', 16 | { 17 | username: this.sequelize.Sequelize.STRING, 18 | beforeTime: { 19 | type: this.sequelize.Sequelize.DATE, 20 | defaultValue: -Infinity 21 | }, 22 | sometime: { 23 | type: this.sequelize.Sequelize.DATE, 24 | defaultValue: this.sequelize.fn('NOW') 25 | }, 26 | anotherTime: { 27 | type: this.sequelize.Sequelize.DATE 28 | }, 29 | afterTime: { 30 | type: this.sequelize.Sequelize.DATE, 31 | defaultValue: Infinity 32 | } 33 | }, 34 | { 35 | timestamps: true 36 | } 37 | ); 38 | 39 | await User.sync({ 40 | force: true 41 | }); 42 | 43 | const user4 = await User.create( 44 | { 45 | username: 'bob', 46 | anotherTime: Infinity 47 | }, 48 | { 49 | validate: true 50 | } 51 | ); 52 | 53 | expect(user4.username).to.equal('bob'); 54 | expect(user4.beforeTime).to.equal(-Infinity); 55 | expect(user4.sometime).to.be.withinTime(date, new Date()); 56 | expect(user4.anotherTime).to.equal(Infinity); 57 | expect(user4.afterTime).to.equal(Infinity); 58 | 59 | const user3 = await user4.update( 60 | { 61 | sometime: Infinity 62 | }, 63 | { 64 | returning: true 65 | } 66 | ); 67 | 68 | expect(user3.sometime).to.equal(Infinity); 69 | 70 | const user2 = await user3.update({ 71 | sometime: Infinity 72 | }); 73 | 74 | expect(user2.sometime).to.equal(Infinity); 75 | 76 | const user1 = await user2.update( 77 | { 78 | sometime: this.sequelize.fn('NOW') 79 | }, 80 | { 81 | returning: true 82 | } 83 | ); 84 | 85 | expect(user1.sometime).to.be.withinTime(date, new Date()); 86 | 87 | // find 88 | const users = await User.findAll(); 89 | expect(users[0].beforeTime).to.equal(-Infinity); 90 | expect(users[0].sometime).to.not.equal(Infinity); 91 | expect(users[0].afterTime).to.equal(Infinity); 92 | 93 | const user0 = await users[0].update({ 94 | sometime: date 95 | }); 96 | 97 | expect(user0.sometime).to.equalTime(date); 98 | 99 | const user = await user0.update({ 100 | sometime: date 101 | }); 102 | 103 | expect(user.sometime).to.equalTime(date); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /tests/dialects_postgres_error_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { expect } = require('chai'), 4 | { Sequelize, DataTypes } = require('../source'), 5 | _ = require('lodash'); 6 | 7 | // Skip reason: CRDB does not support RANGE type. 8 | describe.skip('[POSTGRES Specific] ExclusionConstraintError', () => { 9 | const constraintName = 'overlap_period'; 10 | beforeEach(async function () { 11 | this.Booking = this.sequelize.define('Booking', { 12 | roomNo: DataTypes.INTEGER, 13 | period: DataTypes.RANGE(DataTypes.DATE) 14 | }); 15 | 16 | await this.Booking.sync({ force: true }); 17 | 18 | await this.sequelize.query( 19 | `ALTER TABLE "${this.Booking.tableName}" ADD CONSTRAINT ${constraintName} EXCLUDE USING gist ("roomNo" WITH =, period WITH &&)` 20 | ); 21 | }); 22 | 23 | it('should contain error specific properties', () => { 24 | const errDetails = { 25 | message: 'Exclusion constraint error', 26 | constraint: 'constraint_name', 27 | fields: { field1: 1, field2: [123, 321] }, 28 | table: 'table_name', 29 | parent: new Error('Test error') 30 | }; 31 | const err = new Sequelize.ExclusionConstraintError(errDetails); 32 | 33 | _.each(errDetails, (value, key) => { 34 | expect(value).to.be.deep.equal(err[key]); 35 | }); 36 | }); 37 | 38 | it('should throw ExclusionConstraintError when "period" value overlaps existing', async function () { 39 | const Booking = this.Booking; 40 | 41 | await Booking.create({ 42 | roomNo: 1, 43 | guestName: 'Incognito Visitor', 44 | period: [new Date(2015, 0, 1), new Date(2015, 0, 3)] 45 | }); 46 | 47 | await expect( 48 | Booking.create({ 49 | roomNo: 1, 50 | guestName: 'Frequent Visitor', 51 | period: [new Date(2015, 0, 2), new Date(2015, 0, 5)] 52 | }) 53 | ).to.eventually.be.rejectedWith(Sequelize.ExclusionConstraintError); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/dialects_query_interface_test.js: -------------------------------------------------------------------------------- 1 | // Skip reason: 2 | // CRDB does not have CREATE FUNCTION syntax. 3 | describe.skip('[POSTGRES Specific] QueryInterface', () => { 4 | }); 5 | -------------------------------------------------------------------------------- /tests/dialects_query_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const { expect } = require('chai'), 6 | { Sequelize, DataTypes } = require('../source'); 7 | 8 | const Support = { 9 | // Copied from helper, to attend to a specific Sequelize instance creation. 10 | createSequelizeInstance: options => { 11 | return new Sequelize('sequelize_test', 'root', '', { 12 | dialect: 'postgres', 13 | port: process.env.COCKROACH_PORT || 26257, 14 | logging: false, 15 | typeValidation: true, 16 | minifyAliases: options.minifyAliases || false, 17 | dialectOptions: {cockroachdbTelemetryDisabled : true} 18 | }); 19 | } 20 | }; 21 | 22 | describe('[POSTGRES] Query', function () { 23 | const taskAlias = 24 | 'AnActualVeryLongAliasThatShouldBreakthePostgresLimitOfSixtyFourCharacters'; 25 | const teamAlias = 'Toto'; 26 | 27 | const executeTest = async function (options, test) { 28 | const sequelize = Support.createSequelizeInstance(options); 29 | 30 | const User = sequelize.define( 31 | 'User', 32 | { name: DataTypes.STRING, updatedAt: DataTypes.DATE }, 33 | { underscored: true } 34 | ); 35 | const Team = sequelize.define('Team', { name: DataTypes.STRING }); 36 | const Task = sequelize.define('Task', { title: DataTypes.STRING }); 37 | 38 | User.belongsTo(Task, { as: taskAlias, foreignKey: 'task_id' }); 39 | User.belongsToMany(Team, { 40 | as: teamAlias, 41 | foreignKey: 'teamId', 42 | through: 'UserTeam' 43 | }); 44 | Team.belongsToMany(User, { foreignKey: 'userId', through: 'UserTeam' }); 45 | 46 | await sequelize.sync({ force: true }); 47 | const team = await Team.create({ name: 'rocket' }); 48 | const task = await Task.create({ title: 'SuperTask' }); 49 | const user = await User.create({ 50 | name: 'test', 51 | task_id: task.id, 52 | updatedAt: new Date() 53 | }); 54 | await user[`add${teamAlias}`](team); 55 | 56 | return test( 57 | await User.findOne({ 58 | include: [ 59 | { 60 | model: Task, 61 | as: taskAlias 62 | }, 63 | { 64 | model: Team, 65 | as: teamAlias 66 | } 67 | ] 68 | }) 69 | ); 70 | }; 71 | 72 | // Skip reason: CRDB does support identifiers longer than 64 characters. 73 | it.skip('should throw due to alias being truncated', async function () { 74 | const options = { ...this.sequelize.options, minifyAliases: false }; 75 | 76 | await executeTest(options, res => { 77 | expect(res[taskAlias]).to.not.exist; 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /tests/drop_enum_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | require('./helper'); 16 | 17 | const { expect } = require('chai'); 18 | const { Sequelize, DataTypes } = require('../source'); 19 | 20 | describe('QueryInterface', () => { 21 | beforeEach(function () { 22 | this.sequelize.options.quoteIdentifiers = true; 23 | this.queryInterface = this.sequelize.getQueryInterface(); 24 | }); 25 | 26 | describe('dropEnum', () => { 27 | beforeEach(async function () { 28 | await this.queryInterface.createTable('menus', { 29 | structuretype: DataTypes.ENUM('menus', 'submenu', 'routine'), 30 | sequence: DataTypes.INTEGER, 31 | name: DataTypes.STRING 32 | }); 33 | }); 34 | 35 | it('should be able to drop the specified column', async function () { 36 | await this.queryInterface.removeColumn('menus', 'structuretype'); 37 | const enumList0 = await this.queryInterface.pgListEnums('menus'); 38 | 39 | expect(enumList0).to.have.lengthOf(1); 40 | expect(enumList0[0]) 41 | .to.have.property('enum_name') 42 | .and.to.equal('enum_menus_structuretype'); 43 | }); 44 | 45 | it('should be able to drop the specified enum after removing the column', async function () { 46 | await expect( 47 | this.queryInterface.dropEnum('enum_menus_structuretype') 48 | ).to.be.eventually.rejectedWith( 49 | 'cannot drop type "enum_menus_structuretype" because other objects ([sequelize_test.public.menus]) still depend on it' 50 | ); 51 | 52 | await this.queryInterface.removeColumn('menus', 'structuretype'); 53 | 54 | await this.queryInterface.dropEnum('enum_menus_structuretype'); 55 | 56 | const enumList = await this.queryInterface.pgListEnums('menus'); 57 | 58 | expect(enumList).to.be.an('array'); 59 | expect(enumList).to.have.lengthOf(0); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/enum_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | require('./helper'); 16 | 17 | const { expect } = require('chai'); 18 | const { Sequelize, DataTypes } = require('../source'); 19 | 20 | describe('Enum', function () { 21 | beforeEach(async function () { 22 | this.Bar = this.sequelize.define('bar', { 23 | enum: DataTypes.ENUM('A', 'B') 24 | }); 25 | await this.Bar.sync({ force: true }); 26 | }); 27 | 28 | it('accepts valid values', async function () { 29 | const bar = await this.Bar.create({ enum: 'A' }); 30 | expect(bar.enum).to.equal('A'); 31 | }); 32 | 33 | it('rejects invalid values', async function () { 34 | await expect(this.Bar.create({ enum: 'C' })).to.be.eventually.rejectedWith( 35 | '"C" is not a valid choice in ["A","B"]' 36 | ); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/find_or_create_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | require('./helper'); 16 | 17 | const { expect } = require('chai'); 18 | const { Sequelize, DataTypes } = require('../source'); 19 | 20 | describe('findOrCreate', function () { 21 | it('supports CockroachDB', function () { 22 | expect(Sequelize.supportsCockroachDB).to.be.true; 23 | }); 24 | 25 | it('creates a row when missing', async function () { 26 | const User = this.sequelize.define('user', { 27 | id: { 28 | type: DataTypes.INTEGER, 29 | primaryKey: true 30 | }, 31 | name: { 32 | type: DataTypes.STRING 33 | } 34 | }); 35 | 36 | const id1 = 1; 37 | const origName = 'original'; 38 | 39 | await User.sync({ force: true }); 40 | 41 | const [user, created] = await User.findOrCreate({ 42 | where: { 43 | id: id1, 44 | name: origName 45 | } 46 | }); 47 | 48 | expect(user.name).to.equal(origName); 49 | expect(user.updatedAt).to.equalTime(user.createdAt); 50 | expect(created).to.be.true; 51 | 52 | const userAgain = await User.findByPk(id1); 53 | 54 | expect(userAgain.name).to.equal(origName); 55 | expect(userAgain.updatedAt).to.equalTime(userAgain.createdAt); 56 | }); 57 | 58 | it('finds the row when present', async function () { 59 | const User = this.sequelize.define('user', { 60 | id: { 61 | type: DataTypes.INTEGER, 62 | primaryKey: true 63 | }, 64 | name: { 65 | type: DataTypes.STRING 66 | } 67 | }); 68 | 69 | const id1 = 1; 70 | const origName = 'original'; 71 | const updatedName = 'UPDATED'; 72 | 73 | await User.sync({ force: true }); 74 | 75 | const user = await User.create({ 76 | id: id1, 77 | name: origName 78 | }); 79 | 80 | expect(user.name).to.equal(origName); 81 | expect(user.updatedAt).to.equalTime(user.createdAt); 82 | 83 | const [userAgain, created] = await User.findOrCreate({ 84 | where: { 85 | id: id1 86 | }, 87 | defaults: { 88 | name: updatedName 89 | } 90 | }); 91 | 92 | expect(userAgain.name).to.equal(origName); 93 | expect(userAgain.updatedAt).to.equalTime(userAgain.createdAt); 94 | expect(created).to.be.false; 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /tests/helper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | const chai = require('chai'); 16 | const Sequelize = require('../source'); 17 | 18 | chai.use(require('chai-as-promised')); 19 | chai.use(require('chai-datetime')); 20 | chai.use(require('sinon-chai')); 21 | 22 | // These tests run against a local instance of CockroachDB that meets the 23 | // following requirements: 24 | // 25 | // 1. Running with the --insecure flag. 26 | // 2. Contains a database named "sequelize_test". 27 | 28 | // To override the CockroachDB port, set the COCKROACH_PORT environment 29 | // variable. 30 | 31 | async function cleanupDatabase(sequelize) { 32 | // https://github.com/sequelize/sequelize/blob/29901187d9560e7d51ae1f9b5f411cf0c5d8994a/test/support.js#L136 33 | const qi = sequelize.getQueryInterface(); 34 | await qi.dropAllTables(); 35 | sequelize.modelManager.models = []; 36 | sequelize.models = {}; 37 | if (qi.dropAllEnums) { 38 | await qi.dropAllEnums(); 39 | } 40 | const schemas = await sequelize.showAllSchemas(); 41 | for (const schema of schemas) { 42 | const schemaName = schema.name || schema; 43 | if (schemaName !== sequelize.config.database) { 44 | await sequelize.dropSchema(schemaName); 45 | } 46 | } 47 | } 48 | 49 | before(function () { 50 | this.sequelize = makeTestSequelizeInstance(); 51 | }); 52 | 53 | afterEach(async function () { 54 | await cleanupDatabase(this.sequelize); 55 | }); 56 | 57 | after(async function () { 58 | await this.sequelize.close(); 59 | }); 60 | 61 | function makeTestSequelizeInstance() { 62 | return new Sequelize('sequelize_test', 'root', '', { 63 | dialect: 'postgres', 64 | port: process.env.COCKROACH_PORT || 26257, 65 | logging: false, 66 | typeValidation: true, 67 | dialectOptions: {cockroachdbTelemetryDisabled : true}, 68 | }); 69 | } 70 | 71 | module.exports = { makeTestSequelizeInstance }; 72 | -------------------------------------------------------------------------------- /tests/include_find_all_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const chai = require('chai'), 6 | { DataTypes } = require('../source'), 7 | expect = chai.expect, 8 | _ = require('lodash'); 9 | 10 | describe('Include', () => { 11 | describe('findAll', () => { 12 | beforeEach(function() { 13 | this.fixtureA = async function() { 14 | const User = this.sequelize.define('User', {}), 15 | Company = this.sequelize.define('Company', { 16 | name: DataTypes.STRING 17 | }), 18 | Product = this.sequelize.define('Product', { 19 | title: DataTypes.STRING 20 | }), 21 | Tag = this.sequelize.define('Tag', { 22 | name: DataTypes.STRING 23 | }), 24 | Price = this.sequelize.define('Price', { 25 | value: DataTypes.FLOAT 26 | }), 27 | Customer = this.sequelize.define('Customer', { 28 | name: DataTypes.STRING 29 | }), 30 | Group = this.sequelize.define('Group', { 31 | name: DataTypes.STRING 32 | }), 33 | GroupMember = this.sequelize.define('GroupMember', { 34 | 35 | }), 36 | Rank = this.sequelize.define('Rank', { 37 | name: DataTypes.STRING, 38 | canInvite: { 39 | type: DataTypes.INTEGER, 40 | defaultValue: 0 41 | }, 42 | canRemove: { 43 | type: DataTypes.INTEGER, 44 | defaultValue: 0 45 | }, 46 | canPost: { 47 | type: DataTypes.INTEGER, 48 | defaultValue: 0 49 | } 50 | }); 51 | 52 | this.models = { 53 | User, 54 | Company, 55 | Product, 56 | Tag, 57 | Price, 58 | Customer, 59 | Group, 60 | GroupMember, 61 | Rank 62 | }; 63 | 64 | User.hasMany(Product); 65 | Product.belongsTo(User); 66 | 67 | Product.belongsToMany(Tag, { through: 'product_tag' }); 68 | Tag.belongsToMany(Product, { through: 'product_tag' }); 69 | Product.belongsTo(Tag, { as: 'Category' }); 70 | Product.belongsTo(Company); 71 | 72 | Product.hasMany(Price); 73 | Price.belongsTo(Product); 74 | 75 | User.hasMany(GroupMember, { as: 'Memberships' }); 76 | GroupMember.belongsTo(User); 77 | GroupMember.belongsTo(Rank); 78 | GroupMember.belongsTo(Group); 79 | Group.hasMany(GroupMember, { as: 'Memberships' }); 80 | 81 | await this.sequelize.sync({ force: true }); 82 | await Group.bulkCreate([ 83 | { name: 'Developers' }, 84 | { name: 'Designers' }, 85 | { name: 'Managers' } 86 | ]); 87 | const groups = await Group.findAll(); 88 | await Company.bulkCreate([ 89 | { name: 'Sequelize' }, 90 | { name: 'Coca Cola' }, 91 | { name: 'Bonanza' }, 92 | { name: 'NYSE' }, 93 | { name: 'Coshopr' } 94 | ]); 95 | const companies = await Company.findAll(); 96 | await Rank.bulkCreate([ 97 | { name: 'Admin', canInvite: 1, canRemove: 1, canPost: 1 }, 98 | { name: 'Trustee', canInvite: 1, canRemove: 0, canPost: 1 }, 99 | { name: 'Member', canInvite: 1, canRemove: 0, canPost: 0 } 100 | ]); 101 | const ranks = await Rank.findAll(); 102 | await Tag.bulkCreate([ 103 | { name: 'A' }, 104 | { name: 'B' }, 105 | { name: 'C' }, 106 | { name: 'D' }, 107 | { name: 'E' } 108 | ]); 109 | const tags = await Tag.findAll(); 110 | for (const i of [0, 1, 2, 3, 4]) { 111 | const user = await User.create(); 112 | // Edited this part because a test expect id to be 3. 113 | // This maintains the test procedure, giving Product predictable ids. 114 | await Product.bulkCreate([ 115 | { id: i * 5 + 1, title: 'Chair' }, 116 | { id: i * 5 + 2, title: 'Desk' }, 117 | { id: i * 5 + 3, title: 'Bed' }, 118 | { id: i * 5 + 4, title: 'Pen' }, 119 | { id: i * 5 + 5, title: 'Monitor' } 120 | ]); 121 | const products = await Product.findAll(); 122 | const groupMembers = [ 123 | { AccUserId: user.id, GroupId: groups[0].id, RankId: ranks[0].id }, 124 | { AccUserId: user.id, GroupId: groups[1].id, RankId: ranks[2].id } 125 | ]; 126 | if (i < 3) { 127 | groupMembers.push({ AccUserId: user.id, GroupId: groups[2].id, RankId: ranks[1].id }); 128 | } 129 | await Promise.all([ 130 | GroupMember.bulkCreate(groupMembers), 131 | user.setProducts([ 132 | products[i * 5 + 0], 133 | products[i * 5 + 1], 134 | products[i * 5 + 3] 135 | ]), 136 | products[i * 5 + 0].setTags([ 137 | tags[0], 138 | tags[2] 139 | ]), 140 | products[i * 5 + 1].setTags([ 141 | tags[1] 142 | ]), 143 | products[i * 5 + 0].setCategory(tags[1]), 144 | products[i * 5 + 2].setTags([ 145 | tags[0] 146 | ]), 147 | products[i * 5 + 3].setTags([ 148 | tags[0] 149 | ]), 150 | products[i * 5 + 0].setCompany(companies[4]), 151 | products[i * 5 + 1].setCompany(companies[3]), 152 | products[i * 5 + 2].setCompany(companies[2]), 153 | products[i * 5 + 3].setCompany(companies[1]), 154 | products[i * 5 + 4].setCompany(companies[0]), 155 | Price.bulkCreate([ 156 | { ProductId: products[i * 5 + 0].id, value: 5 }, 157 | { ProductId: products[i * 5 + 0].id, value: 10 }, 158 | { ProductId: products[i * 5 + 1].id, value: 5 }, 159 | { ProductId: products[i * 5 + 1].id, value: 10 }, 160 | { ProductId: products[i * 5 + 1].id, value: 15 }, 161 | { ProductId: products[i * 5 + 1].id, value: 20 }, 162 | { ProductId: products[i * 5 + 2].id, value: 20 }, 163 | { ProductId: products[i * 5 + 3].id, value: 20 } 164 | ]) 165 | ]); 166 | } 167 | }; 168 | }); 169 | 170 | // Edit reason: This test originally fails because it expects ProductId = 3. 171 | // Edited beforeEach to give it predictable ids. 172 | // CRDB does not guarantee that a DB entry will have sequential ids, starting by 1. 173 | it('should be possible to select on columns inside a through table', async function() { 174 | await this.fixtureA(); 175 | 176 | const products = await this.models.Product.findAll({ 177 | attributes: ['title'], 178 | include: [ 179 | { 180 | model: this.models.Tag, 181 | through: { 182 | where: { 183 | ProductId: 3 184 | } 185 | }, 186 | required: true 187 | } 188 | ] 189 | }); 190 | 191 | expect(products).have.length(1); 192 | }); 193 | 194 | // Edit reason: This test originally fails because it expects ProductId = 3. 195 | // Edited beforeEach to give it predictable ids. 196 | // CRDB does not guarantee that a DB entry will have sequential ids, starting by 1. 197 | it('should be possible to select on columns inside a through table and a limit', async function() { 198 | await this.fixtureA(); 199 | 200 | const products = await this.models.Product.findAll({ 201 | attributes: ['title'], 202 | include: [ 203 | { 204 | model: this.models.Tag, 205 | through: { 206 | where: { 207 | ProductId: 3 208 | } 209 | }, 210 | required: true 211 | } 212 | ], 213 | limit: 5 214 | }); 215 | 216 | expect(products).have.length(1); 217 | }); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /tests/include_find_and_count_all_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const { expect } = require('chai'), 6 | { DataTypes, Sequelize } = require('../source'), 7 | sinon = require('sinon'), 8 | Op = Sequelize.Op; 9 | 10 | describe('Include', () => { 11 | before(function() { 12 | this.clock = sinon.useFakeTimers(); 13 | }); 14 | 15 | after(function() { 16 | this.clock.restore(); 17 | }); 18 | 19 | describe('findAndCountAll', () => { 20 | 21 | // Edited test. Reason: CRDB does not guarantee sequential ids. This test relies on predictable ids. 22 | it('should be able to include a required model. Result rows should match count', async function() { 23 | const User = this.sequelize.define('User', { name: DataTypes.STRING(40) }, { paranoid: true }), 24 | SomeConnection = this.sequelize.define('SomeConnection', { 25 | m: DataTypes.STRING(40), 26 | fk: DataTypes.INTEGER, 27 | u: DataTypes.INTEGER 28 | }, { paranoid: true }), 29 | A = this.sequelize.define('A', { name: DataTypes.STRING(40) }, { paranoid: true }), 30 | B = this.sequelize.define('B', { name: DataTypes.STRING(40) }, { paranoid: true }), 31 | C = this.sequelize.define('C', { name: DataTypes.STRING(40) }, { paranoid: true }); 32 | 33 | // Associate them 34 | User.hasMany(SomeConnection, { foreignKey: 'u', constraints: false }); 35 | 36 | SomeConnection.belongsTo(User, { foreignKey: 'u', constraints: false }); 37 | SomeConnection.belongsTo(A, { foreignKey: 'fk', constraints: false }); 38 | SomeConnection.belongsTo(B, { foreignKey: 'fk', constraints: false }); 39 | SomeConnection.belongsTo(C, { foreignKey: 'fk', constraints: false }); 40 | 41 | A.hasMany(SomeConnection, { foreignKey: 'fk', constraints: false }); 42 | B.hasMany(SomeConnection, { foreignKey: 'fk', constraints: false }); 43 | C.hasMany(SomeConnection, { foreignKey: 'fk', constraints: false }); 44 | 45 | // Sync them 46 | await this.sequelize.sync({ force: true }); 47 | 48 | // Create an enviroment 49 | 50 | await Promise.all([User.bulkCreate([ 51 | // This part differs from original Sequelize test. 52 | // Added sequential ids to creation because of Association expectation. 53 | { id: 1, name: 'Youtube' }, 54 | { id: 2, name: 'Facebook' }, 55 | { id: 3, name: 'Google' }, 56 | { id: 4, name: 'Yahoo' }, 57 | { id: 5, name: '404' } 58 | ]), SomeConnection.bulkCreate([ // Lets count, m: A and u: 1 59 | { u: 1, m: 'A', fk: 1 }, // 1 // Will be deleted 60 | { u: 2, m: 'A', fk: 1 }, 61 | { u: 3, m: 'A', fk: 1 }, 62 | { u: 4, m: 'A', fk: 1 }, 63 | { u: 5, m: 'A', fk: 1 }, 64 | { u: 1, m: 'B', fk: 1 }, 65 | { u: 2, m: 'B', fk: 1 }, 66 | { u: 3, m: 'B', fk: 1 }, 67 | { u: 4, m: 'B', fk: 1 }, 68 | { u: 5, m: 'B', fk: 1 }, 69 | { u: 1, m: 'C', fk: 1 }, 70 | { u: 2, m: 'C', fk: 1 }, 71 | { u: 3, m: 'C', fk: 1 }, 72 | { u: 4, m: 'C', fk: 1 }, 73 | { u: 5, m: 'C', fk: 1 }, 74 | { u: 1, m: 'A', fk: 2 }, // 2 // Will be deleted 75 | { u: 4, m: 'A', fk: 2 }, 76 | { u: 2, m: 'A', fk: 2 }, 77 | { u: 1, m: 'A', fk: 3 }, // 3 78 | { u: 2, m: 'A', fk: 3 }, 79 | { u: 3, m: 'A', fk: 3 }, 80 | { u: 2, m: 'B', fk: 2 }, 81 | { u: 1, m: 'A', fk: 4 }, // 4 82 | { u: 4, m: 'A', fk: 2 } 83 | ]), A.bulkCreate([ 84 | // This part differs from original Sequelize test. 85 | // Added sequential ids to creation because of Association expectation. 86 | { id: 1, name: 'Just' }, 87 | { id: 2, name: 'for' }, 88 | { id: 3, name: 'testing' }, 89 | { id: 4, name: 'proposes' }, 90 | { id: 5, name: 'only' } 91 | ]), B.bulkCreate([ 92 | { name: 'this should not' }, 93 | { name: 'be loaded' } 94 | ]), C.bulkCreate([ 95 | { name: 'because we only want A' } 96 | ])]); 97 | 98 | // Delete some of conns to prove the concept 99 | await SomeConnection.destroy({ where: { 100 | m: 'A', 101 | u: 1, 102 | fk: [1, 2] 103 | } }); 104 | 105 | this.clock.tick(1000); 106 | 107 | // Last and most important queries ( we connected 4, but deleted 2, witch means we must get 2 only ) 108 | const result = await A.findAndCountAll({ 109 | include: [{ 110 | model: SomeConnection, required: true, 111 | where: { 112 | m: 'A', // Pseudo Polymorphy 113 | u: 1 114 | } 115 | }], 116 | limit: 5 117 | }); 118 | 119 | expect(result.count).to.be.equal(2); 120 | expect(result.rows.length).to.be.equal(2); 121 | }); 122 | 123 | // Edited test. Reason: CRDB does not guarantee sequential ids. This test relies on predictable ids. 124 | it('should correctly filter, limit and sort when multiple includes and types of associations are present.', async function() { 125 | const TaskTag = this.sequelize.define('TaskTag', { 126 | id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, 127 | name: { type: DataTypes.STRING } 128 | }); 129 | 130 | const Tag = this.sequelize.define('Tag', { 131 | id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, 132 | name: { type: DataTypes.STRING } 133 | }); 134 | 135 | const Task = this.sequelize.define('Task', { 136 | id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, 137 | name: { type: DataTypes.STRING } 138 | }); 139 | const Project = this.sequelize.define('Project', { 140 | id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, 141 | m: { type: DataTypes.STRING } 142 | }); 143 | 144 | const User = this.sequelize.define('User', { 145 | id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, 146 | name: { type: DataTypes.STRING } 147 | }); 148 | 149 | Project.belongsTo(User); 150 | Task.belongsTo(Project); 151 | Task.belongsToMany(Tag, { through: TaskTag }); 152 | 153 | // Sync them 154 | await this.sequelize.sync({ force: true }); 155 | 156 | // Create an enviroment 157 | await User.bulkCreate([ 158 | // This part differs from original Sequelize test. 159 | // Added sequential ids to creation because of Association expectation. 160 | { id: 1, name: 'user-name-1' }, 161 | { id: 2, name: 'user-name-2' } 162 | ]); 163 | 164 | await Project.bulkCreate([ 165 | // This part differs from original Sequelize test. 166 | // Added sequential ids to creation because of Association expectation. 167 | { id: 1, m: 'A', UserId: 1 }, 168 | { id: 2, m: 'A', UserId: 2 } 169 | ]); 170 | 171 | await Task.bulkCreate([ 172 | { ProjectId: 1, name: 'Just' }, 173 | { ProjectId: 1, name: 'for' }, 174 | { ProjectId: 2, name: 'testing' }, 175 | { ProjectId: 2, name: 'proposes' } 176 | ]); 177 | 178 | // Find All Tasks with Project(m=a) and User(name=user-name-2) 179 | const result = await Task.findAndCountAll({ 180 | limit: 1, 181 | offset: 0, 182 | order: [['id', 'DESC']], 183 | include: [ 184 | { 185 | model: Project, 186 | where: { [Op.and]: [{ m: 'A' }] }, 187 | include: [{ 188 | model: User, 189 | where: { [Op.and]: [{ name: 'user-name-2' }] } 190 | } 191 | ] 192 | }, 193 | { model: Tag } 194 | ] 195 | }); 196 | 197 | expect(result.count).to.equal(2); 198 | expect(result.rows.length).to.equal(1); 199 | }); 200 | 201 | // Edited test. Reason: CRDB does not guarantee sequential ids. This test relies on predictable ids. 202 | it('should properly work with sequelize.function', async function() { 203 | const sequelize = this.sequelize; 204 | const User = this.sequelize.define('User', { 205 | id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, 206 | first_name: { type: DataTypes.STRING }, 207 | last_name: { type: DataTypes.STRING } 208 | }); 209 | 210 | const Project = this.sequelize.define('Project', { 211 | id: { type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true }, 212 | name: { type: DataTypes.STRING } 213 | }); 214 | 215 | User.hasMany(Project); 216 | 217 | await this.sequelize.sync({ force: true }); 218 | 219 | await User.bulkCreate([ 220 | // This part differs from original Sequelize test. 221 | // Added sequential ids to creation because of Association expectation. 222 | { id: 1, first_name: 'user-fname-1', last_name: 'user-lname-1' }, 223 | { id: 2, first_name: 'user-fname-2', last_name: 'user-lname-2' }, 224 | { id: 3, first_name: 'user-xfname-1', last_name: 'user-xlname-1' } 225 | ]); 226 | 227 | await Project.bulkCreate([ 228 | { name: 'naam-satya', UserId: 1 }, 229 | { name: 'guru-satya', UserId: 2 }, 230 | { name: 'app-satya', UserId: 2 } 231 | ]); 232 | 233 | const result = await User.findAndCountAll({ 234 | limit: 1, 235 | offset: 1, 236 | where: sequelize.or( 237 | { first_name: { [Op.like]: '%user-fname%' } }, 238 | { last_name: { [Op.like]: '%user-lname%' } } 239 | ), 240 | include: [ 241 | { 242 | model: Project, 243 | required: true, 244 | where: { name: { 245 | [Op.in]: ['naam-satya', 'guru-satya'] 246 | } } 247 | } 248 | ] 249 | }); 250 | 251 | expect(result.count).to.equal(2); 252 | expect(result.rows.length).to.equal(1); 253 | }); 254 | }); 255 | }); 256 | -------------------------------------------------------------------------------- /tests/instance_to_json_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const { expect } = require('chai'), 6 | DataTypes = require('../source'); 7 | 8 | describe('Instance', () => { 9 | describe('toJSON', () => { 10 | beforeEach(async function () { 11 | this.User = this.sequelize.define( 12 | 'User', 13 | { 14 | username: { type: DataTypes.STRING }, 15 | age: DataTypes.INTEGER, 16 | level: { type: DataTypes.INTEGER }, 17 | isUser: { 18 | type: DataTypes.BOOLEAN, 19 | defaultValue: false 20 | }, 21 | isAdmin: { type: DataTypes.BOOLEAN } 22 | }, 23 | { 24 | timestamps: false 25 | } 26 | ); 27 | 28 | this.Project = this.sequelize.define( 29 | 'NiceProject', 30 | { title: DataTypes.STRING }, 31 | { timestamps: false } 32 | ); 33 | 34 | this.User.hasMany(this.Project, { 35 | as: 'Projects', 36 | foreignKey: 'lovelyUserId' 37 | }); 38 | this.Project.belongsTo(this.User, { 39 | as: 'LovelyUser', 40 | foreignKey: 'lovelyUserId' 41 | }); 42 | 43 | await this.User.sync({ force: true }); 44 | 45 | await this.Project.sync({ force: true }); 46 | }); 47 | 48 | describe('create', () => { 49 | // Edited test 50 | // CRDB ids are BigInt by default. This patch treats BigInts as Strings, since BigInts are not serializable. 51 | it('returns a response that can be stringified', async function () { 52 | const user = await this.User.create({ 53 | username: 'test.user', 54 | age: 99, 55 | isAdmin: true, 56 | isUser: false, 57 | level: null 58 | }); 59 | 60 | // changed this to expect a String ID. 61 | expect(JSON.stringify(user)).to.deep.equal( 62 | `{"id":"${user.get( 63 | 'id' 64 | )}","username":"test.user","age":99,"isAdmin":true,"isUser":false,"level":null}` 65 | ); 66 | }); 67 | }); 68 | 69 | describe('find', () => { 70 | // Edited test 71 | // CRDB ids are BigInt by default. This patch treats BigInts as Strings, since BigInts are not serializable. 72 | it('returns a response that can be stringified', async function () { 73 | const user0 = await this.User.create({ 74 | username: 'test.user', 75 | age: 99, 76 | isAdmin: true, 77 | isUser: false 78 | }); 79 | 80 | const user = await this.User.findByPk(user0.get('id')); 81 | 82 | // changed THIS to expect a String ID. 83 | expect(JSON.stringify(user)).to.deep.equal( 84 | `{"id":"${user.get( 85 | 'id' 86 | )}","username":"test.user","age":99,"level":null,"isUser":false,"isAdmin":true}` 87 | ); 88 | }); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/instance_validations_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const chai = require('chai'), 6 | expect = chai.expect, 7 | Sequelize = require('../source'); 8 | 9 | describe('InstanceValidator', () => { 10 | describe('#update', () => { 11 | // Edited test. Changed findByPk parameter. 12 | it('should allow us to update specific columns without tripping the validations', async function() { 13 | const User = this.sequelize.define('model', { 14 | username: Sequelize.STRING, 15 | email: { 16 | type: Sequelize.STRING, 17 | allowNull: false, 18 | validate: { 19 | isEmail: { 20 | msg: 'You must enter a valid email address' 21 | } 22 | } 23 | } 24 | }); 25 | 26 | await User.sync({ force: true }); 27 | const user = await User.create({ username: 'bob', email: 'hello@world.com' }); 28 | 29 | await User 30 | .update({ username: 'toni' }, { where: { id: user.id } }); 31 | 32 | // Edited PK. It was 1, now it is dynamically obtained. 33 | const user0 = await User.findByPk(user.id); 34 | expect(user0.username).to.equal('toni'); 35 | }); 36 | 37 | // Reason: Errors array need DB-level details to be generated. Since it doesn't, 38 | // this test lacks details for its expectations. 39 | // https://github.com/cockroachdb/cockroach/issues/63332 40 | it.skip('should enforce a unique constraint', async function() { 41 | const Model = this.sequelize.define('model', { 42 | uniqueName: { type: Sequelize.STRING, unique: 'uniqueName' } 43 | }); 44 | const records = [ 45 | { uniqueName: 'unique name one' }, 46 | { uniqueName: 'unique name two' } 47 | ]; 48 | await Model.sync({ force: true }); 49 | const instance0 = await Model.create(records[0]); 50 | expect(instance0).to.be.ok; 51 | const instance = await Model.create(records[1]); 52 | expect(instance).to.be.ok; 53 | await Model.update(records[0], { where: { id: instance.id } }) 54 | const err = await expect(Model.update(records[0], { where: { id: instance.id } })).to.be.rejected; 55 | console.log(err); 56 | expect(err).to.be.an.instanceOf(Error); 57 | expect(err.errors).to.have.length(1); 58 | expect(err.errors[0].path).to.include('uniqueName'); 59 | expect(err.errors[0].message).to.include('must be unique'); 60 | }); 61 | 62 | // Reason: Errors array need DB-level details to be generated. Since it doesn't, 63 | // this test lacks details for its expectations. 64 | // https://github.com/sequelize/sequelize/blob/main/lib/dialects/postgres/query.js#L319 65 | it.skip('should allow a custom unique constraint error message', async function() { 66 | const Model = this.sequelize.define('model', { 67 | uniqueName: { 68 | type: Sequelize.STRING, 69 | unique: { msg: 'custom unique error message' } 70 | } 71 | }); 72 | const records = [ 73 | { uniqueName: 'unique name one' }, 74 | { uniqueName: 'unique name two' } 75 | ]; 76 | await Model.sync({ force: true }); 77 | const instance0 = await Model.create(records[0]); 78 | expect(instance0).to.be.ok; 79 | const instance = await Model.create(records[1]); 80 | expect(instance).to.be.ok; 81 | const err = await expect(Model.update(records[0], { where: { id: instance.id } })).to.be.rejected; 82 | expect(err).to.be.an.instanceOf(Error); 83 | expect(err.errors).to.have.length(1); 84 | expect(err.errors[0].path).to.include('uniqueName'); 85 | expect(err.errors[0].message).to.equal('custom unique error message'); 86 | }); 87 | 88 | // Reason: Errors array need DB-level details to be generated. Since it doesn't, 89 | // this test lacks details for its expectations. 90 | // https://github.com/sequelize/sequelize/blob/main/lib/dialects/postgres/query.js#L319 91 | it.skip('should handle multiple unique messages correctly', async function() { 92 | const Model = this.sequelize.define('model', { 93 | uniqueName1: { 94 | type: Sequelize.STRING, 95 | unique: { msg: 'custom unique error message 1' } 96 | }, 97 | uniqueName2: { 98 | type: Sequelize.STRING, 99 | unique: { msg: 'custom unique error message 2' } 100 | } 101 | }); 102 | const records = [ 103 | { uniqueName1: 'unique name one', uniqueName2: 'unique name one' }, 104 | { uniqueName1: 'unique name one', uniqueName2: 'this is ok' }, 105 | { uniqueName1: 'this is ok', uniqueName2: 'unique name one' } 106 | ]; 107 | await Model.sync({ force: true }); 108 | const instance = await Model.create(records[0]); 109 | expect(instance).to.be.ok; 110 | const err0 = await expect(Model.create(records[1])).to.be.rejected; 111 | expect(err0).to.be.an.instanceOf(Error); 112 | expect(err0.errors).to.have.length(1); 113 | expect(err0.errors[0].path).to.include('uniqueName1'); 114 | expect(err0.errors[0].message).to.equal('custom unique error message 1'); 115 | 116 | const err = await expect(Model.create(records[2])).to.be.rejected; 117 | expect(err).to.be.an.instanceOf(Error); 118 | expect(err.errors).to.have.length(1); 119 | expect(err.errors[0].path).to.include('uniqueName2'); 120 | expect(err.errors[0].message).to.equal('custom unique error message 2'); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /tests/int_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | require('./helper'); 16 | 17 | const { expect } = require('chai'); 18 | const { Sequelize, DataTypes } = require('../source'); 19 | 20 | for (const intTypeName of ['integer', 'bigint']) { 21 | const intType = DataTypes[intTypeName.toUpperCase()]; 22 | 23 | describe('DataTypes.' + intType.key, function () { 24 | beforeEach(async function () { 25 | this.Foo = this.sequelize.define('foo', { 26 | i: intType 27 | }); 28 | 29 | await this.Foo.sync({ force: true }); 30 | }); 31 | 32 | it('accepts JavaScript integers', async function () { 33 | const foo = await this.Foo.create({ i: 42 }); 34 | expect(foo.i).to.equal(42); 35 | }); 36 | 37 | // Reason: Not fully supported by Sequelize. 38 | // https://github.com/sequelize/sequelize/issues/10468 39 | it.skip('accepts JavaScript strings that represent 64-bit integers', async function () { 40 | const foo = await this.Foo.create({ i: '9223372036854775807' }); 41 | expect(foo.i).to.equal(9223372036854775807n); 42 | }); 43 | it('accepts JavaScript strings that represent 64-bit integers', async function () { 44 | const foo = await this.Foo.create({ i: '9223372036854775807' }); 45 | expect(foo.i).to.equal('9223372036854775807'); 46 | }); 47 | 48 | it('rejects integers that overflow', async function () { 49 | await expect( 50 | this.Foo.create({ i: '9223372036854775808' }) 51 | ).to.be.eventually.rejectedWith('value out of range'); 52 | }); 53 | 54 | it('rejects garbage', async function () { 55 | await expect( 56 | this.Foo.create({ i: '102.3' }) 57 | ).to.be.eventually.rejectedWith(`"102.3" is not a valid ${intTypeName}`); 58 | }); 59 | 60 | it('rejects dangerous input', async function () { 61 | await expect(this.Foo.create({ i: "'" })).to.be.eventually.rejectedWith( 62 | `"\'" is not a valid ${intTypeName}` 63 | ); 64 | }); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /tests/json_test.js: -------------------------------------------------------------------------------- 1 | require('./helper'); 2 | 3 | const { expect } = require('chai'); 4 | const { DataTypes } = require('../source'); 5 | 6 | describe('model', () => { 7 | describe('json', () => { 8 | beforeEach(async function () { 9 | this.User = this.sequelize.define('User', { 10 | username: DataTypes.STRING, 11 | emergency_contact: DataTypes.JSON, 12 | emergencyContact: DataTypes.JSON 13 | }); 14 | this.Order = this.sequelize.define('Order'); 15 | this.Order.belongsTo(this.User); 16 | 17 | await this.sequelize.sync({ force: true }); 18 | }); 19 | 20 | // Reason: CockroachDB only supports JSONB. Creating a DataTypes.JSON, will create a .JSONB instead 21 | // The test originally expects to.equal(JSON); 22 | it('should tell me that a column is jsonb', async function () { 23 | const table = await this.sequelize.queryInterface.describeTable('Users'); 24 | 25 | expect(table.emergency_contact.type).to.equal('JSONB'); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/model_attributes_field_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const sinon = require('sinon'), 6 | { DataTypes } = require('../source'); 7 | 8 | describe('Model', () => { 9 | before(function () { 10 | this.clock = sinon.useFakeTimers(); 11 | }); 12 | 13 | after(function () { 14 | this.clock.restore(); 15 | }); 16 | 17 | describe('attributes', () => { 18 | describe('field', () => { 19 | beforeEach(async function () { 20 | const queryInterface = this.sequelize.getQueryInterface(); 21 | 22 | this.User = this.sequelize.define( 23 | 'user', 24 | { 25 | id: { 26 | type: DataTypes.INTEGER, 27 | allowNull: false, 28 | primaryKey: true, 29 | autoIncrement: true, 30 | field: 'userId' 31 | }, 32 | name: { 33 | type: DataTypes.STRING, 34 | field: 'full_name' 35 | }, 36 | taskCount: { 37 | type: DataTypes.INTEGER, 38 | field: 'task_count', 39 | defaultValue: 0, 40 | allowNull: false 41 | } 42 | }, 43 | { 44 | tableName: 'users', 45 | timestamps: false 46 | } 47 | ); 48 | 49 | this.Task = this.sequelize.define( 50 | 'task', 51 | { 52 | id: { 53 | type: DataTypes.INTEGER, 54 | allowNull: false, 55 | primaryKey: true, 56 | autoIncrement: true, 57 | field: 'taskId' 58 | }, 59 | title: { 60 | type: DataTypes.STRING, 61 | field: 'name' 62 | } 63 | }, 64 | { 65 | tableName: 'tasks', 66 | timestamps: false 67 | } 68 | ); 69 | 70 | this.Comment = this.sequelize.define( 71 | 'comment', 72 | { 73 | id: { 74 | type: DataTypes.INTEGER, 75 | allowNull: false, 76 | primaryKey: true, 77 | autoIncrement: true, 78 | field: 'commentId' 79 | }, 80 | text: { type: DataTypes.STRING, field: 'comment_text' }, 81 | notes: { type: DataTypes.STRING, field: 'notes' }, 82 | likes: { type: DataTypes.INTEGER, field: 'like_count' }, 83 | createdAt: { 84 | type: DataTypes.DATE, 85 | field: 'created_at', 86 | allowNull: false 87 | }, 88 | updatedAt: { 89 | type: DataTypes.DATE, 90 | field: 'updated_at', 91 | allowNull: false 92 | } 93 | }, 94 | { 95 | tableName: 'comments', 96 | timestamps: true 97 | } 98 | ); 99 | 100 | this.User.hasMany(this.Task, { 101 | foreignKey: 'user_id' 102 | }); 103 | this.Task.belongsTo(this.User, { 104 | foreignKey: 'user_id' 105 | }); 106 | this.Task.hasMany(this.Comment, { 107 | foreignKey: 'task_id' 108 | }); 109 | this.Comment.belongsTo(this.Task, { 110 | foreignKey: 'task_id' 111 | }); 112 | 113 | this.User.belongsToMany(this.Comment, { 114 | foreignKey: 'userId', 115 | otherKey: 'commentId', 116 | through: 'userComments' 117 | }); 118 | 119 | await Promise.all([ 120 | queryInterface.createTable('users', { 121 | userId: { 122 | type: DataTypes.INTEGER, 123 | allowNull: false, 124 | primaryKey: true, 125 | autoIncrement: true 126 | }, 127 | full_name: { 128 | type: DataTypes.STRING 129 | }, 130 | task_count: { 131 | type: DataTypes.INTEGER, 132 | allowNull: false, 133 | defaultValue: 0 134 | } 135 | }), 136 | queryInterface.createTable('tasks', { 137 | taskId: { 138 | type: DataTypes.INTEGER, 139 | allowNull: false, 140 | primaryKey: true, 141 | autoIncrement: true 142 | }, 143 | user_id: { 144 | type: DataTypes.INTEGER 145 | }, 146 | name: { 147 | type: DataTypes.STRING 148 | } 149 | }), 150 | queryInterface.createTable('comments', { 151 | commentId: { 152 | type: DataTypes.INTEGER, 153 | allowNull: false, 154 | primaryKey: true, 155 | autoIncrement: true 156 | }, 157 | task_id: { 158 | type: DataTypes.INTEGER 159 | }, 160 | comment_text: { 161 | type: DataTypes.STRING 162 | }, 163 | notes: { 164 | type: DataTypes.STRING 165 | }, 166 | like_count: { 167 | type: DataTypes.INTEGER 168 | }, 169 | created_at: { 170 | type: DataTypes.DATE, 171 | allowNull: false 172 | }, 173 | updated_at: { 174 | type: DataTypes.DATE 175 | } 176 | }), 177 | queryInterface.createTable('userComments', { 178 | commentId: { 179 | type: DataTypes.INTEGER 180 | }, 181 | userId: { 182 | type: DataTypes.INTEGER 183 | } 184 | }) 185 | ]); 186 | }); 187 | 188 | describe('field and attribute name is the same', () => { 189 | beforeEach(async function () { 190 | await this.Comment.bulkCreate([ 191 | // Added ids to Comment 192 | { id: 1, notes: 'Number one' }, 193 | { id: 2, notes: 'Number two' } 194 | ]); 195 | }); 196 | // Edited beforeEach of this test since it looks for Pk 1, and it's not guaranteed id will be 1. 197 | it('reload should work', async function () { 198 | const comment = await this.Comment.findByPk(1); 199 | await comment.reload(); 200 | }); 201 | }); 202 | }); 203 | }); 204 | }); 205 | -------------------------------------------------------------------------------- /tests/model_bulk_create_test.js: -------------------------------------------------------------------------------- 1 | require('./helper'); 2 | 3 | const { expect } = require('chai'); 4 | const { DataTypes } = require('../source'); 5 | 6 | describe('Model', () => { 7 | beforeEach(async function () { 8 | this.User = this.sequelize.define('User', { 9 | username: DataTypes.STRING, 10 | secretValue: { 11 | type: DataTypes.STRING, 12 | field: 'secret_value' 13 | }, 14 | data: DataTypes.STRING, 15 | intVal: DataTypes.INTEGER, 16 | theDate: DataTypes.DATE, 17 | aBool: DataTypes.BOOLEAN, 18 | uniqueName: { type: DataTypes.STRING, unique: true } 19 | }); 20 | this.Account = this.sequelize.define('Account', { 21 | accountName: DataTypes.STRING 22 | }); 23 | this.Student = this.sequelize.define('Student', { 24 | no: { type: DataTypes.INTEGER, primaryKey: true }, 25 | name: { type: DataTypes.STRING, allowNull: false } 26 | }); 27 | this.Car = this.sequelize.define('Car', { 28 | plateNumber: { 29 | type: DataTypes.STRING, 30 | primaryKey: true, 31 | field: 'plate_number' 32 | }, 33 | color: { 34 | type: DataTypes.TEXT 35 | } 36 | }); 37 | 38 | await this.sequelize.sync({ force: true }); 39 | }); 40 | 41 | describe('bulkCreate', () => { 42 | it.skip('supports transactions', async function () { 43 | const User = this.sequelize.define('User', { 44 | username: DataTypes.STRING 45 | }); 46 | await User.sync({ force: true }); 47 | const transaction = await this.sequelize.transaction(); 48 | await User.bulkCreate([{ username: 'foo' }, { username: 'bar' }], { 49 | transaction 50 | }); 51 | const count1 = await User.count(); 52 | const count2 = await User.count({ transaction }); 53 | expect(count1).to.equal(0); 54 | expect(count2).to.equal(2); 55 | await transaction.rollback(); 56 | }); 57 | 58 | // Reason: CRDB does not guarantee autoIncrement to be sequential. 59 | // Reimplementing the test to check if it is incremental below. 60 | describe('return values', () => { 61 | it.skip('should make the auto incremented values available on the returned instances', async function () { 62 | const User = this.sequelize.define('user', {}); 63 | 64 | await User.sync({ force: true }); 65 | 66 | const users0 = await User.bulkCreate([{}, {}, {}], { returning: true }); 67 | 68 | const actualUsers0 = await User.findAll({ order: ['id'] }); 69 | const [users, actualUsers] = [users0, actualUsers0]; 70 | expect(users.length).to.eql(actualUsers.length); 71 | users.forEach((user, i) => { 72 | expect(user.get('id')).to.be.ok; 73 | expect(user.get('id')) 74 | .to.equal(actualUsers[i].get('id')) 75 | .and.to.equal(i + 1); 76 | }); 77 | }); 78 | it('should make the auto incremented values available on the returned instances', async function () { 79 | const User = this.sequelize.define('user', {}); 80 | 81 | await User.sync({ force: true }); 82 | 83 | const users = await User.bulkCreate([{}, {}, {}], { returning: true }); 84 | 85 | const actualUsers = await User.findAll({ order: ['id'] }); 86 | 87 | const usersIds = users.map(user => user.get('id')); 88 | const actualUserIds = actualUsers.map(user => user.get('id')); 89 | const orderedUserIds = usersIds.sort((a, b) => a - b); 90 | 91 | users.forEach(user => expect(user.get('id')).to.be.ok); 92 | expect(usersIds).to.eql(actualUserIds); 93 | expect(usersIds).to.eql(orderedUserIds); 94 | }); 95 | 96 | // Reason: CRDB does not guarantee autoIncrement to be sequential. 97 | // Reimplementing the test to check if it is incremental below. 98 | it.skip('should make the auto incremented values available on the returned instances with custom fields', async function () { 99 | const User = this.sequelize.define('user', { 100 | maId: { 101 | type: DataTypes.INTEGER, 102 | primaryKey: true, 103 | autoIncrement: true, 104 | field: 'yo_id' 105 | } 106 | }); 107 | 108 | await User.sync({ force: true }); 109 | 110 | const users = await User.bulkCreate([{}, {}, {}], { returning: true }); 111 | 112 | const actualUsers = await User.findAll({ order: ['maId'] }); 113 | 114 | expect(users.length).to.eql(actualUsers.length); 115 | users.forEach((user, i) => { 116 | expect(user.get('maId')).to.be.ok; 117 | expect(user.get('maId')) 118 | .to.equal(actualUsers[i].get('maId')) 119 | .and.to.equal(i + 1); 120 | }); 121 | }); 122 | it('should make the auto incremented values available on the returned instances with custom fields', async function () { 123 | const User = this.sequelize.define('user', { 124 | maId: { 125 | type: DataTypes.INTEGER, 126 | primaryKey: true, 127 | autoIncrement: true, 128 | field: 'yo_id' 129 | } 130 | }); 131 | 132 | await User.sync({ force: true }); 133 | 134 | const users = await User.bulkCreate([{}, {}, {}], { returning: true }); 135 | 136 | const actualUsers = await User.findAll({ order: ['maId'] }); 137 | 138 | const usersIds = users.map(user => user.get('maId')); 139 | const actualUserIds = actualUsers.map(user => user.get('maId')); 140 | const orderedUserIds = usersIds.sort((a, b) => a - b); 141 | 142 | users.forEach(user => expect(user.get('maId')).to.be.ok); 143 | expect(usersIds).to.eql(actualUserIds); 144 | expect(usersIds).to.eql(orderedUserIds); 145 | }); 146 | }); 147 | 148 | describe('handles auto increment values', () => { 149 | // Reason: CRDB does not guarantee autoIncrement to be sequential. 150 | // Reimplementing the test to check if it is incremental below. 151 | it.skip('should return auto increment primary key values', async function () { 152 | const Maya = this.sequelize.define('Maya', {}); 153 | 154 | const M1 = {}; 155 | const M2 = {}; 156 | 157 | await Maya.sync({ force: true }); 158 | const ms = await Maya.bulkCreate([M1, M2], { returning: true }); 159 | expect(ms[0].id).to.be.eql(1); 160 | expect(ms[1].id).to.be.eql(2); 161 | }); 162 | it('should return auto increment primary key values', async function () { 163 | const Maya = this.sequelize.define('Maya', {}); 164 | 165 | const M1 = {}; 166 | const M2 = {}; 167 | 168 | await Maya.sync({ force: true }); 169 | const ms = await Maya.bulkCreate([M1, M2], { returning: true }); 170 | 171 | expect(ms[0].id < ms[1].id).to.be.true; 172 | }); 173 | }); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /tests/model_find_all_group_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const { expect } = require('chai'); 6 | const { Sequelize, DataTypes } = require('../source'); 7 | 8 | describe('Model', () => { 9 | describe('findAll', () => { 10 | describe('group', () => { 11 | it('should correctly group with attributes, #3009', async function () { 12 | const Post = this.sequelize.define('Post', { 13 | id: { 14 | type: DataTypes.INTEGER, 15 | autoIncrement: true, 16 | primaryKey: true 17 | }, 18 | name: { type: DataTypes.STRING, allowNull: false } 19 | }); 20 | 21 | const Comment = this.sequelize.define('Comment', { 22 | id: { 23 | type: DataTypes.INTEGER, 24 | autoIncrement: true, 25 | primaryKey: true 26 | }, 27 | text: { type: DataTypes.STRING, allowNull: false } 28 | }); 29 | 30 | Post.hasMany(Comment); 31 | 32 | await this.sequelize.sync({ force: true }); 33 | 34 | // CRDB does not give human readable ids, it's usually a Big Number. 35 | // Also, autoIncrement does not guarantee sequentially incremented numbers. 36 | // Had to ensure ids are 1 and 2 for this test. 37 | await Post.bulkCreate([ 38 | { id: 1, name: 'post-1' }, 39 | { id: 2, name: 'post-2' } 40 | ]); 41 | 42 | await Comment.bulkCreate([ 43 | { text: 'Market', PostId: 1 }, 44 | { text: 'Text', PostId: 2 }, 45 | { text: 'Abc', PostId: 2 }, 46 | { text: 'Semaphor', PostId: 1 }, 47 | { text: 'Text', PostId: 1 } 48 | ]); 49 | 50 | const posts = await Post.findAll({ 51 | attributes: [ 52 | [ 53 | Sequelize.fn('COUNT', Sequelize.col('Comments.id')), 54 | 'comment_count' 55 | ] 56 | ], 57 | include: [{ model: Comment, attributes: [] }], 58 | group: ['Post.id'], 59 | order: [['id']] 60 | }); 61 | 62 | expect(parseInt(posts[0].get('comment_count'), 10)).to.be.equal(3); 63 | expect(parseInt(posts[1].get('comment_count'), 10)).to.be.equal(2); 64 | }); 65 | 66 | it('should not add primary key when grouping using a belongsTo association', async function () { 67 | const Post = this.sequelize.define('Post', { 68 | id: { 69 | type: DataTypes.INTEGER, 70 | autoIncrement: true, 71 | primaryKey: true 72 | }, 73 | name: { type: DataTypes.STRING, allowNull: false } 74 | }); 75 | 76 | const Comment = this.sequelize.define('Comment', { 77 | id: { 78 | type: DataTypes.INTEGER, 79 | autoIncrement: true, 80 | primaryKey: true 81 | }, 82 | text: { type: DataTypes.STRING, allowNull: false } 83 | }); 84 | 85 | Post.hasMany(Comment); 86 | Comment.belongsTo(Post); 87 | 88 | await this.sequelize.sync({ force: true }); 89 | 90 | // CRDB does not give human readable ids, it's usually a Big Number. 91 | // Also, autoIncrement does not guarantee sequentially incremented numbers. 92 | // Had to ensure ids are 1 and 2 for this test. 93 | await Post.bulkCreate([ 94 | { id: 1, name: 'post-1' }, 95 | { id: 2, name: 'post-2' } 96 | ]); 97 | 98 | await Comment.bulkCreate([ 99 | { text: 'Market', PostId: 1 }, 100 | { text: 'Text', PostId: 2 }, 101 | { text: 'Abc', PostId: 2 }, 102 | { text: 'Semaphor', PostId: 1 }, 103 | { text: 'Text', PostId: 1 } 104 | ]); 105 | 106 | const posts = await Comment.findAll({ 107 | attributes: [ 108 | 'PostId', 109 | [ 110 | Sequelize.fn('COUNT', Sequelize.col('Comment.id')), 111 | 'comment_count' 112 | ] 113 | ], 114 | include: [{ model: Post, attributes: [] }], 115 | group: ['PostId'], 116 | order: [['PostId']] 117 | }); 118 | 119 | expect(posts[0].get().hasOwnProperty('id')).to.equal(false); 120 | expect(posts[1].get().hasOwnProperty('id')).to.equal(false); 121 | expect(parseInt(posts[0].get('comment_count'), 10)).to.be.equal(3); 122 | expect(parseInt(posts[1].get('comment_count'), 10)).to.be.equal(2); 123 | }); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /tests/model_find_all_order_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const DataTypes = require('../source'); 6 | 7 | describe('Model', () => { 8 | describe('findAll', () => { 9 | describe('order', () => { 10 | describe('injections', () => { 11 | beforeEach(async function () { 12 | this.User = this.sequelize.define('user', { 13 | name: DataTypes.STRING 14 | }); 15 | this.Group = this.sequelize.define('group', {}); 16 | this.User.belongsTo(this.Group); 17 | await this.sequelize.sync({ force: true }); 18 | }); 19 | 20 | // Reason: Not implemented yet. 21 | // https://www.cockroachlabs.com/docs/stable/null-handling.html#nulls-and-sorting 22 | it.skip('should not throw with on NULLS LAST/NULLS FIRST', async function () { 23 | await this.User.findAll({ 24 | include: [this.Group], 25 | order: [ 26 | ['id', 'ASC NULLS LAST'], 27 | [this.Group, 'id', 'DESC NULLS FIRST'] 28 | ] 29 | }); 30 | }); 31 | }); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/model_find_one_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const { expect } = require('chai'), 6 | { Sequelize, DataTypes } = require('../source'); 7 | 8 | const config = { 9 | rand: () => parseInt(Math.random() * 999, 10) 10 | }; 11 | 12 | describe('Model', () => { 13 | beforeEach(async function () { 14 | this.User = this.sequelize.define('User', { 15 | username: DataTypes.STRING, 16 | secretValue: DataTypes.STRING, 17 | data: DataTypes.STRING, 18 | intVal: DataTypes.INTEGER, 19 | theDate: DataTypes.DATE, 20 | aBool: DataTypes.BOOLEAN 21 | }); 22 | 23 | await this.User.sync({ force: true }); 24 | }); 25 | 26 | describe('findOne', () => { 27 | it.skip('supports transactions', async function () { 28 | const User = this.sequelize.define('User', { 29 | username: Sequelize.STRING 30 | }); 31 | 32 | await User.sync({ force: true }); 33 | const t = await this.sequelize.transaction(); 34 | await User.create({ username: 'foo' }, { transaction: t }); 35 | 36 | const user1 = await User.findOne({ 37 | where: { username: 'foo' } 38 | }); 39 | 40 | const user2 = await User.findOne({ 41 | where: { username: 'foo' }, 42 | transaction: t 43 | }); 44 | 45 | expect(user1).to.be.null; 46 | expect(user2).to.not.be.null; 47 | await t.rollback(); 48 | }); 49 | 50 | describe('general / basic function', () => { 51 | beforeEach(async function () { 52 | const user = await this.User.create({ username: 'barfooz' }); 53 | this.UserPrimary = this.sequelize.define('UserPrimary', { 54 | specialkey: { 55 | type: DataTypes.STRING, 56 | primaryKey: true 57 | } 58 | }); 59 | 60 | await this.UserPrimary.sync({ force: true }); 61 | await this.UserPrimary.create({ specialkey: 'a string' }); 62 | this.user = user; 63 | }); 64 | 65 | // Edited Test 66 | // Reason: This test expected id to be 1. 67 | // CRDB does not work with human-readable ids by default. 68 | it('returns a single dao', async function () { 69 | const user = await this.User.findByPk(this.user.id); 70 | expect(Array.isArray(user)).to.not.be.ok; 71 | expect(user.id).to.equal(this.user.id); 72 | }); 73 | 74 | // Edited Test 75 | // Reason: This test expected id to be 1. 76 | // CRDB does not work with human-readable ids by default. 77 | it('returns a single dao given a string id', async function () { 78 | const user = await this.User.findByPk(this.user.id.toString()); 79 | expect(Array.isArray(user)).to.not.be.ok; 80 | expect(user.id).to.equal(this.user.id); 81 | }); 82 | 83 | // Edited test 84 | // Reason: This test tries to find id 1, which does not exist. 85 | it('should make aliased attributes available', async function () { 86 | const user = await this.User.findOne({ 87 | // used the id from beforeEach created user 88 | where: { id: this.user.id }, 89 | attributes: ['id', ['username', 'name']] 90 | }); 91 | 92 | expect(user.dataValues.name).to.equal('barfooz'); 93 | }); 94 | 95 | // Edited test 96 | // Reason: CRDB does not work with human-readable ids. 97 | it('should allow us to find IDs using capital letters', async function () { 98 | const User = this.sequelize.define(`User${config.rand()}`, { 99 | ID: { 100 | type: Sequelize.INTEGER, 101 | primaryKey: true, 102 | autoIncrement: true 103 | }, 104 | Login: { type: Sequelize.STRING } 105 | }); 106 | 107 | await User.sync({ force: true }); 108 | await User.create({ ID: 1, Login: 'foo' }); 109 | const user = await User.findByPk(1); 110 | expect(user).to.exist; 111 | expect(user.ID).to.equal(1); 112 | }); 113 | 114 | // Reason: CockroachDB does not yet support CITEXT 115 | // Seen here: https://github.com/cockroachdb/cockroach/issues/22463 116 | it.skip('should allow case-insensitive find on CITEXT type', async function () { 117 | const User = this.sequelize.define('UserWithCaseInsensitiveName', { 118 | username: Sequelize.CITEXT 119 | }); 120 | 121 | await User.sync({ force: true }); 122 | await User.create({ username: 'longUserNAME' }); 123 | const user = await User.findOne({ 124 | where: { username: 'LONGusername' } 125 | }); 126 | expect(user).to.exist; 127 | expect(user.username).to.equal('longUserNAME'); 128 | }); 129 | 130 | // Reason: CockroachDB does not yet support TSVECTOR 131 | // Seen here: https://github.com/cockroachdb/cockroach/issues/41288 132 | it.skip('should allow case-sensitive find on TSVECTOR type', async function () { 133 | const User = this.sequelize.define('UserWithCaseInsensitiveName', { 134 | username: Sequelize.TSVECTOR 135 | }); 136 | 137 | await User.sync({ force: true }); 138 | await User.create({ username: 'longUserNAME' }); 139 | const user = await User.findOne({ 140 | where: { username: 'longUserNAME' } 141 | }); 142 | expect(user).to.exist; 143 | expect(user.username).to.equal("'longUserNAME'"); 144 | }); 145 | }); 146 | 147 | describe('rejectOnEmpty mode', () => { 148 | // Edited test 149 | // Reason: This test uses originally a number which is neither a valid Int or BigInt. 150 | // Edited the PK to be zero, so it will be not found and achieve the test purpose. 151 | it('throws error when record not found by findByPk', async function () { 152 | // 4732322332323333232344334354234 originally on Sequelize test suite 153 | await expect( 154 | this.User.findByPk(0, { 155 | rejectOnEmpty: true 156 | }) 157 | ).to.eventually.be.rejectedWith(Sequelize.EmptyResultError); 158 | }); 159 | }); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /tests/model_increment_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const { expect } = require('chai'), 6 | DataTypes = require('../source'), 7 | sinon = require('sinon'); 8 | 9 | describe('Model', () => { 10 | before(function () { 11 | this.clock = sinon.useFakeTimers(); 12 | }); 13 | 14 | after(function () { 15 | this.clock.restore(); 16 | }); 17 | 18 | ['increment', 'decrement'].forEach(method => { 19 | describe(method, () => { 20 | before(function () { 21 | this.assert = (increment, decrement) => { 22 | return method === 'increment' ? increment : decrement; 23 | }; 24 | }); 25 | 26 | // Edited test: 27 | // Refactored to dynamically get the created user id. 28 | it('with timestamps set to true', async function () { 29 | const User = this.sequelize.define( 30 | 'IncrementUser', 31 | { 32 | aNumber: DataTypes.INTEGER 33 | }, 34 | { timestamps: true } 35 | ); 36 | 37 | await User.sync({ force: true }); 38 | const createdUser = await User.create({ aNumber: 1 }); 39 | const oldDate = createdUser.updatedAt; 40 | 41 | this.clock.tick(1000); 42 | await User[method]('aNumber', { by: 1, where: {} }); 43 | 44 | // Removed .eventually method, from chai-as-promised. 45 | const foundUser = await User.findByPk(createdUser.id); 46 | await expect(foundUser) 47 | .to.have.property('updatedAt') 48 | .afterTime(oldDate); 49 | }); 50 | 51 | // Edited test: 52 | // Refactored to dynamically get the created user id. 53 | it('with timestamps set to true and options.silent set to true', async function () { 54 | const User = this.sequelize.define( 55 | 'IncrementUser', 56 | { 57 | aNumber: DataTypes.INTEGER 58 | }, 59 | { timestamps: true } 60 | ); 61 | 62 | await User.sync({ force: true }); 63 | const createdUser = await User.create({ aNumber: 1 }); 64 | const oldDate = createdUser.updatedAt; 65 | 66 | this.clock.tick(1000); 67 | await User[method]('aNumber', { by: 1, silent: true, where: {} }); 68 | 69 | // Removed .eventually method, from chai-as-promised. 70 | const foundUser = await User.findByPk(createdUser.id); 71 | await expect(foundUser) 72 | .to.have.property('updatedAt') 73 | .equalTime(oldDate); 74 | }); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tests/model_update_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | require('./helper'); 16 | 17 | const { expect } = require('chai'); 18 | const { Sequelize, DataTypes } = require('../source'); 19 | const sinon = require('sinon'); 20 | 21 | describe('Model', () => { 22 | describe('update', () => { 23 | beforeEach(async function () { 24 | this.Account = this.sequelize.define('Account', { 25 | ownerId: { 26 | type: DataTypes.INTEGER, 27 | allowNull: false, 28 | field: 'owner_id' 29 | }, 30 | name: { 31 | type: DataTypes.STRING 32 | } 33 | }); 34 | await this.Account.sync({ force: true }); 35 | }); 36 | 37 | it('should only update the passed fields', async function () { 38 | const spy = sinon.spy(); 39 | 40 | const account = await this.Account.create({ ownerId: 2 }); 41 | 42 | await this.Account.update( 43 | { 44 | name: Math.random().toString() 45 | }, 46 | { 47 | where: { 48 | id: account.get('id') 49 | }, 50 | logging: spy 51 | } 52 | ); 53 | 54 | // The substring `ownerId` should not be found in the logged SQL 55 | expect( 56 | spy, 57 | 'Update query was issued when no data to update' 58 | ).to.have.not.been.calledWithMatch('ownerId'); 59 | }); 60 | 61 | describe('skips update query', () => { 62 | it('if no data to update', async function () { 63 | const spy = sinon.spy(); 64 | 65 | await this.Account.create({ ownerId: 3 }); 66 | 67 | const result = await this.Account.update( 68 | { 69 | unknownField: 'haha' 70 | }, 71 | { 72 | where: { 73 | ownerId: 3 74 | }, 75 | logging: spy 76 | } 77 | ); 78 | 79 | expect(result[0]).to.equal(0); 80 | expect(spy, 'Update query was issued when no data to update').to.have 81 | .not.been.called; 82 | }); 83 | 84 | it('skips when timestamps disabled', async function () { 85 | const Model = this.sequelize.define( 86 | 'Model', 87 | { 88 | ownerId: { 89 | type: DataTypes.INTEGER, 90 | allowNull: false, 91 | field: 'owner_id' 92 | }, 93 | name: { 94 | type: DataTypes.STRING 95 | } 96 | }, 97 | { 98 | timestamps: false 99 | } 100 | ); 101 | const spy = sinon.spy(); 102 | 103 | await Model.sync({ force: true }); 104 | await Model.create({ ownerId: 3 }); 105 | 106 | const result = await Model.update( 107 | { 108 | unknownField: 'haha' 109 | }, 110 | { 111 | where: { 112 | ownerId: 3 113 | }, 114 | logging: spy 115 | } 116 | ); 117 | 118 | expect(result[0]).to.equal(0); 119 | expect(spy, 'Update query was issued when no data to update').to.have 120 | .not.been.called; 121 | }); 122 | }); 123 | 124 | it('changed should be false after reload', async function () { 125 | const account0 = await this.Account.create({ ownerId: 2, name: 'foo' }); 126 | account0.name = 'bar'; 127 | expect(account0.changed()[0]).to.equal('name'); 128 | const account = await account0.reload(); 129 | expect(account.changed()).to.equal(false); 130 | }); 131 | 132 | it('should ignore undefined values without throwing not null validation', async function () { 133 | const ownerId = 2; 134 | 135 | const account0 = await this.Account.create({ 136 | ownerId, 137 | name: Math.random().toString() 138 | }); 139 | 140 | await this.Account.update( 141 | { 142 | name: Math.random().toString(), 143 | ownerId: undefined 144 | }, 145 | { 146 | where: { 147 | id: account0.get('id') 148 | } 149 | } 150 | ); 151 | 152 | const account = await this.Account.findOne(); 153 | expect(account.ownerId).to.be.equal(ownerId); 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /tests/operators_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./helper'); 4 | 5 | const { expect } = require('chai'), 6 | { DataTypes, Sequelize } = require('../source'), 7 | Op = Sequelize.Op; 8 | 9 | describe('Operators', () => { 10 | describe('REGEXP', () => { 11 | beforeEach(async function () { 12 | this.User = this.sequelize.define( 13 | 'user', 14 | { 15 | id: { 16 | type: DataTypes.INTEGER, 17 | allowNull: false, 18 | primaryKey: true, 19 | autoIncrement: true, 20 | field: 'userId' 21 | }, 22 | name: { 23 | type: DataTypes.STRING, 24 | field: 'full_name' 25 | } 26 | }, 27 | { 28 | tableName: 'users', 29 | timestamps: false 30 | } 31 | ); 32 | 33 | await this.sequelize.getQueryInterface().createTable('users', { 34 | userId: { 35 | type: DataTypes.INTEGER, 36 | allowNull: false, 37 | primaryKey: true, 38 | autoIncrement: true 39 | }, 40 | full_name: { 41 | type: DataTypes.STRING 42 | } 43 | }); 44 | }); 45 | 46 | describe('case sensitive', () => { 47 | it('should work with a regexp where', async function () { 48 | await this.User.create({ name: 'Foobar' }); 49 | const user = await this.User.findOne({ 50 | where: { 51 | name: { [Op.regexp]: '^Foo' } 52 | } 53 | }); 54 | expect(user).to.be.ok; 55 | }); 56 | 57 | it('should work with a not regexp where', async function () { 58 | await this.User.create({ name: 'Foobar' }); 59 | const user = await this.User.findOne({ 60 | where: { 61 | name: { [Op.notRegexp]: '^Foo' } 62 | } 63 | }); 64 | expect(user).to.not.be.ok; 65 | }); 66 | 67 | it('should properly escape regular expressions', async function () { 68 | await this.User.bulkCreate([{ name: 'John' }, { name: 'Bob' }]); 69 | await this.User.findAll({ 70 | where: { 71 | name: { [Op.notRegexp]: "Bob'; drop table users --" } 72 | } 73 | }); 74 | await this.User.findAll({ 75 | where: { 76 | name: { [Op.regexp]: "Bob'; drop table users --" } 77 | } 78 | }); 79 | expect(await this.User.findAll()).to.have.length(2); 80 | }); 81 | }); 82 | 83 | describe('case insensitive', () => { 84 | it('should work with a case-insensitive regexp where', async function () { 85 | await this.User.create({ name: 'Foobar' }); 86 | const user = await this.User.findOne({ 87 | where: { 88 | name: { [Op.iRegexp]: '^foo' } 89 | } 90 | }); 91 | expect(user).to.be.ok; 92 | }); 93 | 94 | it('should work with a case-insensitive not regexp where', async function () { 95 | await this.User.create({ name: 'Foobar' }); 96 | const user = await this.User.findOne({ 97 | where: { 98 | name: { [Op.notIRegexp]: '^foo' } 99 | } 100 | }); 101 | expect(user).to.not.be.ok; 102 | }); 103 | 104 | it('should properly escape regular expressions', async function () { 105 | await this.User.bulkCreate([{ name: 'John' }, { name: 'Bob' }]); 106 | await this.User.findAll({ 107 | where: { 108 | name: { [Op.iRegexp]: "Bob'; drop table users --" } 109 | } 110 | }); 111 | await this.User.findAll({ 112 | where: { 113 | name: { [Op.notIRegexp]: "Bob'; drop table users --" } 114 | } 115 | }); 116 | expect(await this.User.findAll()).to.have.length(2); 117 | }); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /tests/pool_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { expect } = require('chai'); 4 | const dialect = 'postgres'; 5 | const sinon = require('sinon'); 6 | const delay = require('delay'); 7 | const Support = require('./support'); 8 | 9 | function assertSameConnection(newConnection, oldConnection) { 10 | expect(oldConnection.processID).to.be.equal(newConnection.processID).and.to.be.ok; 11 | } 12 | 13 | function assertNewConnection(newConnection, oldConnection) { 14 | expect(oldConnection.processID).to.not.be.equal(newConnection.processID); 15 | } 16 | 17 | function attachMSSQLUniqueId(connection) { 18 | return connection; 19 | } 20 | 21 | // PG emits events to communicate with the pg client. 22 | // CRDB apparently does not emit events like these, needed to compare processID: 23 | // https://github.com/brianc/node-postgres/blob/master/packages/pg/lib/client.js#L185 24 | describe.skip('Pooling', () => { 25 | if (dialect === 'sqlite' || process.env.DIALECT === 'postgres-native') return; 26 | 27 | beforeEach(function() { 28 | this.sinon = sinon.createSandbox(); 29 | }); 30 | 31 | afterEach(function() { 32 | this.sinon.restore(); 33 | }); 34 | 35 | describe('network / connection errors', () => { 36 | it('should obtain new connection when old connection is abruptly closed', async () => { 37 | function simulateUnexpectedError(connection) { 38 | connection.emit('error', { code: 'ECONNRESET' }); 39 | } 40 | 41 | const sequelize = Support.createSequelizeInstance({ 42 | pool: { max: 1, idle: 5000 } 43 | }); 44 | const cm = sequelize.connectionManager; 45 | await sequelize.sync(); 46 | 47 | const firstConnection = await cm.getConnection(); 48 | simulateUnexpectedError(firstConnection); 49 | const secondConnection = await cm.getConnection(); 50 | 51 | assertNewConnection(secondConnection, firstConnection); 52 | expect(cm.pool.size).to.equal(1); 53 | expect(cm.validate(firstConnection)).to.be.not.ok; 54 | 55 | await cm.releaseConnection(secondConnection); 56 | }); 57 | 58 | it('should obtain new connection when released connection dies inside pool', async () => { 59 | function simulateUnexpectedError(connection) { 60 | if (dialect === 'postgres') { 61 | connection.end(); 62 | } else { 63 | connection.close(); 64 | } 65 | } 66 | 67 | const sequelize = Support.createSequelizeInstance({ 68 | pool: { max: 1, idle: 5000 } 69 | }); 70 | const cm = sequelize.connectionManager; 71 | await sequelize.sync(); 72 | 73 | const oldConnection = await cm.getConnection(); 74 | await cm.releaseConnection(oldConnection); 75 | simulateUnexpectedError(oldConnection); 76 | const newConnection = await cm.getConnection(); 77 | 78 | assertNewConnection(newConnection, oldConnection); 79 | expect(cm.pool.size).to.equal(1); 80 | expect(cm.validate(oldConnection)).to.be.not.ok; 81 | 82 | await cm.releaseConnection(newConnection); 83 | }); 84 | }); 85 | 86 | describe('idle', () => { 87 | it('should maintain connection within idle range', async () => { 88 | const sequelize = Support.createSequelizeInstance({ 89 | pool: { max: 1, idle: 100 } 90 | }); 91 | const cm = sequelize.connectionManager; 92 | await sequelize.sync(); 93 | 94 | const firstConnection = await cm.getConnection(); 95 | 96 | // TODO - Do we really need this call? 97 | attachMSSQLUniqueId(firstConnection); 98 | 99 | // returning connection back to pool 100 | await cm.releaseConnection(firstConnection); 101 | 102 | // Wait a little and then get next available connection 103 | await delay(90); 104 | const secondConnection = await cm.getConnection(); 105 | 106 | assertSameConnection(secondConnection, firstConnection); 107 | expect(cm.validate(firstConnection)).to.be.ok; 108 | 109 | await cm.releaseConnection(secondConnection); 110 | }); 111 | 112 | it('should get new connection beyond idle range', async () => { 113 | const sequelize = Support.createSequelizeInstance({ 114 | pool: { max: 1, idle: 100, evict: 10 } 115 | }); 116 | const cm = sequelize.connectionManager; 117 | await sequelize.sync(); 118 | 119 | const firstConnection = await cm.getConnection(); 120 | 121 | // TODO - Do we really need this call? 122 | attachMSSQLUniqueId(firstConnection); 123 | 124 | // returning connection back to pool 125 | await cm.releaseConnection(firstConnection); 126 | 127 | // Wait a little and then get next available connection 128 | await delay(110); 129 | 130 | const secondConnection = await cm.getConnection(); 131 | 132 | assertNewConnection(secondConnection, firstConnection); 133 | expect(cm.validate(firstConnection)).not.to.be.ok; 134 | 135 | await cm.releaseConnection(secondConnection); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /tests/remove_column_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { expect } = require('chai'); 4 | const { DataTypes } = require('../source'); 5 | 6 | const Support = { 7 | dropTestSchemas: async sequelize => { 8 | const schemas = await sequelize.showAllSchemas(); 9 | const schemasPromise = []; 10 | schemas.forEach(schema => { 11 | const schemaName = schema.name ? schema.name : schema; 12 | if (schemaName !== sequelize.config.database) { 13 | schemasPromise.push(sequelize.dropSchema(schemaName)); 14 | } 15 | }); 16 | 17 | await Promise.all(schemasPromise.map(p => p.catch(e => e))); 18 | } 19 | }; 20 | 21 | describe('QueryInterface', () => { 22 | beforeEach(function () { 23 | this.sequelize.options.quoteIdenifiers = true; 24 | this.queryInterface = this.sequelize.getQueryInterface(); 25 | }); 26 | 27 | afterEach(async function () { 28 | await Support.dropTestSchemas(this.sequelize); 29 | }); 30 | 31 | describe('removeColumn', () => { 32 | describe('(without a schema)', () => { 33 | beforeEach(async function () { 34 | await this.queryInterface.createTable('users', { 35 | id: { 36 | type: DataTypes.INTEGER, 37 | primaryKey: true, 38 | autoIncrement: true 39 | }, 40 | firstName: { 41 | type: DataTypes.STRING, 42 | defaultValue: 'Someone' 43 | }, 44 | lastName: { 45 | type: DataTypes.STRING 46 | }, 47 | manager: { 48 | type: DataTypes.INTEGER, 49 | references: { 50 | model: 'users', 51 | key: 'id' 52 | } 53 | }, 54 | email: { 55 | type: DataTypes.STRING, 56 | unique: true 57 | } 58 | }); 59 | }); 60 | 61 | // Reason: In CockroachDB, dropping a Primary Key column is restricted. 62 | it.skip('should be able to remove a column with primaryKey', async function () { 63 | await this.queryInterface.removeColumn('users', 'manager'); 64 | const table0 = await this.queryInterface.describeTable('users'); 65 | expect(table0).to.not.have.property('manager'); 66 | try { 67 | await this.queryInterface.removeColumn('users', 'id'); 68 | } catch (err) { 69 | console.log(err); 70 | } 71 | const table = await this.queryInterface.describeTable('users'); 72 | expect(table).to.not.have.property('id'); 73 | }); 74 | }); 75 | 76 | describe('(with a schema)', () => { 77 | beforeEach(async function () { 78 | await this.sequelize.createSchema('archive'); 79 | 80 | await this.queryInterface.createTable( 81 | { 82 | tableName: 'users', 83 | schema: 'archive' 84 | }, 85 | { 86 | id: { 87 | type: DataTypes.INTEGER, 88 | primaryKey: true, 89 | autoIncrement: true 90 | }, 91 | firstName: { 92 | type: DataTypes.STRING, 93 | defaultValue: 'Someone' 94 | }, 95 | lastName: { 96 | type: DataTypes.STRING 97 | }, 98 | email: { 99 | type: DataTypes.STRING, 100 | unique: true 101 | } 102 | } 103 | ); 104 | }); 105 | 106 | // Reason: In CockroachDB, dropping a Primary Key column is restricted. 107 | it.skip('should be able to remove a column with primaryKey', async function () { 108 | await this.queryInterface.removeColumn( 109 | { 110 | tableName: 'users', 111 | schema: 'archive' 112 | }, 113 | 'id' 114 | ); 115 | 116 | const table = await this.queryInterface.describeTable({ 117 | tableName: 'users', 118 | schema: 'archive' 119 | }); 120 | 121 | expect(table).to.not.have.property('id'); 122 | }); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /tests/run_tests/getTestsToIgnore.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | path = require('path'), 3 | readline = require('readline'); 4 | const version_helper = require ('../../source/version_helper.js') 5 | const semver = require('semver'); 6 | 7 | const sharedIgnoredTestsPath = './../.github/workflows/ignore_tests/shared'; 8 | 9 | function parseFilesForTests(files) { 10 | return files.map(async file => { 11 | const rl = readline.createInterface({ 12 | input: fs.createReadStream(file), 13 | crlfDelay: Infinity 14 | }); 15 | 16 | const arr = []; 17 | 18 | for await (const line of rl) { 19 | arr.push(line); 20 | } 21 | 22 | return arr; 23 | }) 24 | } 25 | 26 | function getTestNames() { 27 | var files = fs.readdirSync(sharedIgnoredTestsPath).map(f => { 28 | return path.join(sharedIgnoredTestsPath, f); 29 | }); 30 | 31 | const sequelizeVersion = version_helper.GetSequelizeVersion() 32 | if (semver.satisfies(sequelizeVersion, '<=5')) { 33 | const v5IgnoredTestsPath = './../.github/workflows/ignore_tests/v5'; 34 | var v5files = fs.readdirSync(v5IgnoredTestsPath) 35 | files = files.concat( 36 | v5files.map(f => { 37 | return path.join(v5IgnoredTestsPath, f); 38 | }) 39 | ); 40 | } 41 | 42 | return Promise.all(parseFilesForTests(files)).then(arr => arr.flat().join('|')) 43 | } 44 | 45 | module.exports = getTestNames; 46 | -------------------------------------------------------------------------------- /tests/run_tests/runTests.js: -------------------------------------------------------------------------------- 1 | // This runner instance is meant to be used by CI. 2 | // Paths shown here are aligned to match that implementation. 3 | 4 | const Mocha = require('mocha'), 5 | getTestsToIgnore = require('./getTestsToIgnore'); 6 | 7 | async function makeMocha() { 8 | const testsToIgnore = getTestsToIgnore(); 9 | 10 | const mocha = new Mocha({ 11 | grep: testsToIgnore, 12 | checkLeaks: true, 13 | reporter: 'spec', 14 | timeout: 30000, 15 | invert: true 16 | }); 17 | 18 | const patchPath = './.cockroachdb-patches/index.js'; 19 | const testPath = `./../.downloaded-sequelize/test/integration/${process.env.TEST_PATH}.test.js`; 20 | 21 | mocha.addFile(patchPath); 22 | mocha.addFile(testPath); 23 | 24 | return mocha; 25 | } 26 | 27 | // Run the tests. 28 | makeMocha().then(mocha => 29 | mocha.run(function (failures) { 30 | process.exit(failures ? 1 : 0); // exit with non-zero status if there were failures 31 | }) 32 | ); 33 | -------------------------------------------------------------------------------- /tests/scope_test.js: -------------------------------------------------------------------------------- 1 | require('./helper'); 2 | 3 | var expect = require('chai').expect; 4 | var Sequelize = require('..'); 5 | var DataTypes = Sequelize.DataTypes; 6 | var Op = Sequelize.Op; 7 | 8 | describe('associations', () => { 9 | describe('scope', () => { 10 | beforeEach(function () { 11 | this.Post = this.sequelize.define('post', {}); 12 | this.Image = this.sequelize.define('image', {}); 13 | this.Question = this.sequelize.define('question', {}); 14 | this.Comment = this.sequelize.define('comment', { 15 | title: Sequelize.STRING, 16 | type: Sequelize.STRING, 17 | commentable: Sequelize.STRING, 18 | commentable_id: Sequelize.INTEGER, 19 | isMain: { 20 | type: Sequelize.BOOLEAN, 21 | defaultValue: false 22 | } 23 | }); 24 | 25 | this.Comment.prototype.getItem = function () { 26 | return this[ 27 | `get${this.get('commentable').substr(0, 1).toUpperCase()}${this.get( 28 | 'commentable' 29 | ).substr(1)}` 30 | ](); 31 | }; 32 | 33 | this.Post.addScope('withComments', { 34 | include: [this.Comment] 35 | }); 36 | this.Post.addScope('withMainComment', { 37 | include: [ 38 | { 39 | model: this.Comment, 40 | as: 'mainComment' 41 | } 42 | ] 43 | }); 44 | this.Post.hasMany(this.Comment, { 45 | foreignKey: 'commentable_id', 46 | scope: { 47 | commentable: 'post' 48 | }, 49 | constraints: false 50 | }); 51 | this.Post.hasMany(this.Comment, { 52 | foreignKey: 'commentable_id', 53 | as: 'coloredComments', 54 | scope: { 55 | commentable: 'post', 56 | type: { [Op.in]: ['blue', 'green'] } 57 | }, 58 | constraints: false 59 | }); 60 | this.Post.hasOne(this.Comment, { 61 | foreignKey: 'commentable_id', 62 | as: 'mainComment', 63 | scope: { 64 | commentable: 'post', 65 | isMain: true 66 | }, 67 | constraints: false 68 | }); 69 | this.Comment.belongsTo(this.Post, { 70 | foreignKey: 'commentable_id', 71 | as: 'post', 72 | constraints: false 73 | }); 74 | 75 | this.Image.hasMany(this.Comment, { 76 | foreignKey: 'commentable_id', 77 | scope: { 78 | commentable: 'image' 79 | }, 80 | constraints: false 81 | }); 82 | this.Comment.belongsTo(this.Image, { 83 | foreignKey: 'commentable_id', 84 | as: 'image', 85 | constraints: false 86 | }); 87 | 88 | this.Question.hasMany(this.Comment, { 89 | foreignKey: 'commentable_id', 90 | scope: { 91 | commentable: 'question' 92 | }, 93 | constraints: false 94 | }); 95 | this.Comment.belongsTo(this.Question, { 96 | foreignKey: 'commentable_id', 97 | as: 'question', 98 | constraints: false 99 | }); 100 | }); 101 | 102 | describe('N:M', () => { 103 | // Reason: Scope association fails when using UUID 104 | // https://github.com/sequelize/sequelize/issues/13072 105 | describe.skip('on the through model', () => { 106 | beforeEach(function () { 107 | this.Post = this.sequelize.define('post', {}); 108 | this.Image = this.sequelize.define('image', {}); 109 | this.Question = this.sequelize.define('question', {}); 110 | 111 | this.ItemTag = this.sequelize.define('item_tag', { 112 | id: { 113 | type: DataTypes.INTEGER, 114 | primaryKey: true, 115 | autoIncrement: true 116 | }, 117 | tag_id: { 118 | type: DataTypes.INTEGER, 119 | unique: 'item_tag_taggable' 120 | }, 121 | taggable: { 122 | type: DataTypes.STRING, 123 | unique: 'item_tag_taggable' 124 | }, 125 | taggable_id: { 126 | type: DataTypes.INTEGER, 127 | unique: 'item_tag_taggable', 128 | references: null 129 | } 130 | }); 131 | this.Tag = this.sequelize.define('tag', { 132 | name: DataTypes.STRING 133 | }); 134 | 135 | this.Post.belongsToMany(this.Tag, { 136 | through: { 137 | model: this.ItemTag, 138 | unique: false, 139 | scope: { 140 | taggable: 'post' 141 | } 142 | }, 143 | foreignKey: 'taggable_id', 144 | constraints: false 145 | }); 146 | this.Tag.belongsToMany(this.Post, { 147 | through: { 148 | model: this.ItemTag, 149 | unique: false 150 | }, 151 | foreignKey: 'tag_id' 152 | }); 153 | 154 | this.Image.belongsToMany(this.Tag, { 155 | through: { 156 | model: this.ItemTag, 157 | unique: false, 158 | scope: { 159 | taggable: 'image' 160 | } 161 | }, 162 | foreignKey: 'taggable_id', 163 | constraints: false 164 | }); 165 | this.Tag.belongsToMany(this.Image, { 166 | through: { 167 | model: this.ItemTag, 168 | unique: false 169 | }, 170 | foreignKey: 'tag_id' 171 | }); 172 | 173 | this.Question.belongsToMany(this.Tag, { 174 | through: { 175 | model: this.ItemTag, 176 | unique: false, 177 | scope: { 178 | taggable: 'question' 179 | } 180 | }, 181 | foreignKey: 'taggable_id', 182 | constraints: false 183 | }); 184 | this.Tag.belongsToMany(this.Question, { 185 | through: { 186 | model: this.ItemTag, 187 | unique: false 188 | }, 189 | foreignKey: 'tag_id' 190 | }); 191 | }); 192 | 193 | it('should create, find and include associations with scope values', async function () { 194 | await Promise.all([ 195 | this.Post.sync({ force: true }), 196 | this.Image.sync({ force: true }), 197 | this.Question.sync({ force: true }), 198 | this.Tag.sync({ force: true }) 199 | ]); 200 | 201 | await this.ItemTag.sync({ force: true }); 202 | 203 | const [ 204 | post0, 205 | image0, 206 | question0, 207 | tagA, 208 | tagB, 209 | tagC 210 | ] = await Promise.all([ 211 | this.Post.create(), 212 | this.Image.create(), 213 | this.Question.create(), 214 | this.Tag.create({ name: 'tagA' }), 215 | this.Tag.create({ name: 'tagB' }), 216 | this.Tag.create({ name: 'tagC' }) 217 | ]); 218 | 219 | this.post = post0; 220 | this.image = image0; 221 | this.question = question0; 222 | 223 | await Promise.all([ 224 | post0.setTags([tagA]).then(async () => { 225 | return Promise.all([ 226 | post0.createTag({ name: 'postTag' }), 227 | post0.addTag(tagB) 228 | ]); 229 | }), 230 | image0.setTags([tagB]).then(async () => { 231 | return Promise.all([ 232 | image0.createTag({ name: 'imageTag' }), 233 | image0.addTag(tagC) 234 | ]); 235 | }), 236 | question0.setTags([tagC]).then(async () => { 237 | return Promise.all([ 238 | question0.createTag({ name: 'questionTag' }), 239 | question0.addTag(tagA) 240 | ]); 241 | }) 242 | ]); 243 | 244 | const [postTags, imageTags, questionTags] = await Promise.all([ 245 | this.post.getTags(), 246 | this.image.getTags(), 247 | this.question.getTags() 248 | ]); 249 | expect(postTags.length).to.equal(3); 250 | expect(imageTags.length).to.equal(3); 251 | expect(questionTags.length).to.equal(3); 252 | 253 | expect( 254 | postTags 255 | .map(tag => { 256 | return tag.name; 257 | }) 258 | .sort() 259 | ).to.deep.equal(['postTag', 'tagA', 'tagB']); 260 | 261 | expect( 262 | imageTags 263 | .map(tag => { 264 | return tag.name; 265 | }) 266 | .sort() 267 | ).to.deep.equal(['imageTag', 'tagB', 'tagC']); 268 | 269 | expect( 270 | questionTags 271 | .map(tag => { 272 | return tag.name; 273 | }) 274 | .sort() 275 | ).to.deep.equal(['questionTag', 'tagA', 'tagC']); 276 | 277 | const [post, image, question] = await Promise.all([ 278 | this.Post.findOne({ 279 | where: {}, 280 | include: [this.Tag] 281 | }), 282 | this.Image.findOne({ 283 | where: {}, 284 | include: [this.Tag] 285 | }), 286 | this.Question.findOne({ 287 | where: {}, 288 | include: [this.Tag] 289 | }) 290 | ]); 291 | 292 | expect(post.tags.length).to.equal(3); 293 | expect(image.tags.length).to.equal(3); 294 | expect(question.tags.length).to.equal(3); 295 | 296 | expect( 297 | post.tags 298 | .map(tag => { 299 | return tag.name; 300 | }) 301 | .sort() 302 | ).to.deep.equal(['postTag', 'tagA', 'tagB']); 303 | 304 | expect( 305 | image.tags 306 | .map(tag => { 307 | return tag.name; 308 | }) 309 | .sort() 310 | ).to.deep.equal(['imageTag', 'tagB', 'tagC']); 311 | 312 | expect( 313 | question.tags 314 | .map(tag => { 315 | return tag.name; 316 | }) 317 | .sort() 318 | ).to.deep.equal(['questionTag', 'tagA', 'tagC']); 319 | }); 320 | }); 321 | }); 322 | }); 323 | }); 324 | -------------------------------------------------------------------------------- /tests/sequelize_deferrable_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { expect } = require('chai'), 4 | { Sequelize } = require('../source'); 5 | 6 | const config = { 7 | rand: () => { 8 | return parseInt(Math.random() * 999, 10); 9 | } 10 | }; 11 | 12 | describe('Sequelize', () => { 13 | // Skip reason: 14 | // It seems CRDB's CREATE TABLE queries does not support DEFERRABLE syntax. 15 | describe.skip('Deferrable', () => { 16 | const describeDeferrableTest = (title, defineModels) => { 17 | describe(title, () => { 18 | beforeEach(function () { 19 | this.run = async function (deferrable, options) { 20 | options = options || {}; 21 | 22 | const taskTableName = 23 | options.taskTableName || `tasks_${config.rand()}`; 24 | const transactionOptions = { 25 | deferrable: Sequelize.Deferrable.SET_DEFERRED, 26 | ...options 27 | }; 28 | const userTableName = `users_${config.rand()}`; 29 | 30 | const { Task, User } = await defineModels({ 31 | sequelize: this.sequelize, 32 | userTableName, 33 | deferrable, 34 | taskTableName 35 | }); 36 | 37 | return this.sequelize.transaction(transactionOptions, async t => { 38 | const task0 = await Task.create( 39 | { title: 'a task', user_id: -1 }, 40 | { transaction: t } 41 | ); 42 | 43 | const [task, user] = await Promise.all([ 44 | task0, 45 | User.create({}, { transaction: t }) 46 | ]); 47 | task.user_id = user.id; 48 | return task.save({ transaction: t }); 49 | }); 50 | }; 51 | }); 52 | 53 | describe('NOT', () => { 54 | it('does not allow the violation of the foreign key constraint', async function () { 55 | await expect( 56 | this.run(Sequelize.Deferrable.NOT) 57 | ).to.eventually.be.rejectedWith( 58 | Sequelize.ForeignKeyConstraintError 59 | ); 60 | }); 61 | }); 62 | 63 | describe('INITIALLY_IMMEDIATE', () => { 64 | it('allows the violation of the foreign key constraint if the transaction is deferred', async function () { 65 | const task = await this.run( 66 | Sequelize.Deferrable.INITIALLY_IMMEDIATE 67 | ); 68 | 69 | expect(task.title).to.equal('a task'); 70 | expect(task.user_id).to.equal(1); 71 | }); 72 | 73 | it('does not allow the violation of the foreign key constraint if the transaction is not deffered', async function () { 74 | await expect( 75 | this.run(Sequelize.Deferrable.INITIALLY_IMMEDIATE, { 76 | deferrable: undefined 77 | }) 78 | ).to.eventually.be.rejectedWith( 79 | Sequelize.ForeignKeyConstraintError 80 | ); 81 | }); 82 | 83 | it('allows the violation of the foreign key constraint if the transaction deferres only the foreign key constraint', async function () { 84 | const taskTableName = `tasks_${config.rand()}`; 85 | 86 | const task = await this.run( 87 | Sequelize.Deferrable.INITIALLY_IMMEDIATE, 88 | { 89 | deferrable: Sequelize.Deferrable.SET_DEFERRED([ 90 | `${taskTableName}_user_id_fkey` 91 | ]), 92 | taskTableName 93 | } 94 | ); 95 | 96 | expect(task.title).to.equal('a task'); 97 | expect(task.user_id).to.equal(1); 98 | }); 99 | }); 100 | 101 | describe('INITIALLY_DEFERRED', () => { 102 | it('allows the violation of the foreign key constraint', async function () { 103 | const task = await this.run( 104 | Sequelize.Deferrable.INITIALLY_DEFERRED 105 | ); 106 | 107 | expect(task.title).to.equal('a task'); 108 | expect(task.user_id).to.equal(1); 109 | }); 110 | }); 111 | }); 112 | }; 113 | 114 | describeDeferrableTest( 115 | 'set in define', 116 | async ({ sequelize, userTableName, deferrable, taskTableName }) => { 117 | const User = sequelize.define( 118 | 'User', 119 | { name: Sequelize.STRING }, 120 | { tableName: userTableName } 121 | ); 122 | 123 | const Task = sequelize.define( 124 | 'Task', 125 | { 126 | title: Sequelize.STRING, 127 | user_id: { 128 | allowNull: false, 129 | type: Sequelize.INTEGER, 130 | references: { 131 | model: userTableName, 132 | key: 'id', 133 | deferrable 134 | } 135 | } 136 | }, 137 | { 138 | tableName: taskTableName 139 | } 140 | ); 141 | 142 | await User.sync({ force: true }); 143 | await Task.sync({ force: true }); 144 | 145 | return { Task, User }; 146 | } 147 | ); 148 | 149 | describeDeferrableTest( 150 | 'set in addConstraint', 151 | async ({ sequelize, userTableName, deferrable, taskTableName }) => { 152 | const User = sequelize.define( 153 | 'User', 154 | { name: Sequelize.STRING }, 155 | { tableName: userTableName } 156 | ); 157 | 158 | const Task = sequelize.define( 159 | 'Task', 160 | { 161 | title: Sequelize.STRING, 162 | user_id: { 163 | allowNull: false, 164 | type: Sequelize.INTEGER 165 | } 166 | }, 167 | { 168 | tableName: taskTableName 169 | } 170 | ); 171 | 172 | await User.sync({ force: true }); 173 | await Task.sync({ force: true }); 174 | 175 | await sequelize.getQueryInterface().addConstraint(taskTableName, { 176 | fields: ['user_id'], 177 | type: 'foreign key', 178 | name: `${taskTableName}_user_id_fkey`, 179 | deferrable, 180 | references: { 181 | table: userTableName, 182 | field: 'id' 183 | } 184 | }); 185 | 186 | return { Task, User }; 187 | } 188 | ); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /tests/sequelize_query_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { expect } = require('chai'); 4 | const { Sequelize, DataTypes } = require('../source'); 5 | const dialect = 'postgres'; 6 | const sinon = require('sinon'); 7 | const semver = require('semver'); 8 | const version_helper = require('../source/version_helper.js') 9 | const crdbVersion = version_helper.GetCockroachDBVersionFromEnvConfig() 10 | const isCRDBVersion21_1Plus = crdbVersion ? semver.gte(crdbVersion, "21.1.0") : false 11 | 12 | const qq = str => { 13 | if (dialect === 'postgres' || dialect === 'mssql') { 14 | return `"${str}"`; 15 | } 16 | if (dialect === 'mysql' || dialect === 'mariadb' || dialect === 'sqlite') { 17 | return `\`${str}\``; 18 | } 19 | return str; 20 | }; 21 | 22 | const Support = { 23 | // Copied from helper, to attend to a specific Sequelize instance creation. 24 | createSequelizeInstance: options => { 25 | return new Sequelize('sequelize_test', 'root', '', { 26 | dialect: 'postgres', 27 | port: process.env.COCKROACH_PORT || 26257, 28 | logging: false, 29 | typeValidation: true, 30 | benchmark: options.benchmark || false, 31 | logQueryParameters: options.logQueryParameters || false, 32 | minifyAliases: options.minifyAliases || false, 33 | dialectOptions: {cockroachdbTelemetryDisabled : true} 34 | }); 35 | } 36 | }; 37 | 38 | describe('Sequelize', () => { 39 | describe('query', () => { 40 | afterEach(function () { 41 | this.sequelize.options.quoteIdentifiers = true; 42 | console.log.restore && console.log.restore(); 43 | }); 44 | 45 | beforeEach(async function () { 46 | this.User = this.sequelize.define('User', { 47 | username: { 48 | type: Sequelize.STRING, 49 | unique: true 50 | }, 51 | emailAddress: { 52 | type: Sequelize.STRING, 53 | field: 'email_address' 54 | } 55 | }); 56 | 57 | this.insertQuery = `INSERT INTO ${qq( 58 | this.User.tableName 59 | )} (username, email_address, ${qq('createdAt')}, ${qq( 60 | 'updatedAt' 61 | )}) VALUES ('john', 'john@gmail.com', '2012-01-01 10:10:10', '2012-01-01 10:10:10')`; 62 | 63 | await this.User.sync({ force: true }); 64 | }); 65 | 66 | describe('retry', () => { 67 | // Edited test: 68 | // CRDB does not generate Details field, so Sequelize does not describe error as 69 | // Validation error. Changed retry's matcher to match CRDB error. 70 | // https://github.com/cockroachdb/cockroach/issues/63332 71 | it('properly bind parameters on extra retries', async function () { 72 | const payload = { 73 | username: 'test', 74 | createdAt: '2010-10-10 00:00:00', 75 | updatedAt: '2010-10-10 00:00:00' 76 | }; 77 | 78 | const spy = sinon.spy(); 79 | 80 | await this.User.create(payload); 81 | 82 | await expect( 83 | this.sequelize.query( 84 | ` 85 | INSERT INTO ${qq(this.User.tableName)} (username,${qq( 86 | 'createdAt' 87 | )},${qq('updatedAt')}) VALUES ($username,$createdAt,$updatedAt); 88 | `, 89 | { 90 | bind: payload, 91 | logging: spy, 92 | retry: { 93 | max: 3, 94 | // PG matcher 95 | // match: [/Validation/] 96 | // CRDB matcher 97 | match: [/violates unique constraint/] 98 | } 99 | } 100 | ) 101 | ).to.be.rejectedWith(Sequelize.UniqueConstraintError); 102 | 103 | if (isCRDBVersion21_1Plus) { 104 | expect(spy.callCount).to.eql(1); 105 | } else { 106 | expect(spy.callCount).to.eql(3); 107 | } 108 | }); 109 | }); 110 | 111 | describe('logging', () => { 112 | describe('with logQueryParameters', () => { 113 | beforeEach(async function () { 114 | this.sequelize = Support.createSequelizeInstance({ 115 | benchmark: true, 116 | logQueryParameters: true 117 | }); 118 | this.User = this.sequelize.define( 119 | 'User', 120 | { 121 | id: { 122 | type: DataTypes.INTEGER, 123 | primaryKey: true, 124 | autoIncrement: true 125 | }, 126 | username: { 127 | type: DataTypes.STRING 128 | }, 129 | emailAddress: { 130 | type: DataTypes.STRING 131 | } 132 | }, 133 | { 134 | timestamps: false 135 | } 136 | ); 137 | 138 | await this.User.sync({ force: true }); 139 | }); 140 | 141 | // Edit Reason: 142 | // CRDB does not guarantee that ID will start at 1. 143 | it('add parameters in log sql', async function () { 144 | let createSql, updateSql; 145 | 146 | const user = await this.User.create( 147 | { 148 | username: 'john', 149 | emailAddress: 'john@gmail.com' 150 | }, 151 | { 152 | logging: s => { 153 | createSql = s; 154 | } 155 | } 156 | ); 157 | 158 | user.username = 'li'; 159 | 160 | await user.save({ 161 | logging: s => { 162 | updateSql = s; 163 | } 164 | }); 165 | 166 | expect(createSql).to.match( 167 | /; ("john", "john@gmail.com"|{"(\$1|0)":"john","(\$2|1)":"john@gmail.com"})/ 168 | ); 169 | // Edited REGEX. ID is not guaranteed to be 1. 170 | // Will match a CRDB ID between quotes (String), as this adapter treats it. 171 | expect(updateSql).to.match( 172 | /; ("li", "[0-9]+"|{"(\$1|0)":"li","(\$2|1)":1})/ 173 | ); 174 | }); 175 | }); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /tests/sequelize_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { expect, assert } = require('chai'); 4 | const { DataTypes, Sequelize } = require('../source'); 5 | const dialect = 'postgres'; 6 | const _ = require('lodash'); 7 | 8 | const qq = str => { 9 | if (dialect === 'postgres' || dialect === 'mssql') { 10 | return `"${str}"`; 11 | } 12 | if (dialect === 'mysql' || dialect === 'mariadb' || dialect === 'sqlite') { 13 | return `\`${str}\``; 14 | } 15 | return str; 16 | }; 17 | 18 | const config = { 19 | rand: () => { 20 | return parseInt(Math.random() * 999, 10); 21 | } 22 | }; 23 | 24 | describe('Sequelize', () => { 25 | describe('truncate', () => { 26 | // Edit reason: 27 | // CRDB does not guarantee ids to start at 1 28 | it('truncates all models', async function () { 29 | const Project = this.sequelize.define(`project${config.rand()}`, { 30 | id: { 31 | type: DataTypes.INTEGER, 32 | primaryKey: true, 33 | autoIncrement: true 34 | }, 35 | title: DataTypes.STRING 36 | }); 37 | 38 | await this.sequelize.sync({ force: true }); 39 | // Added ID, as it is expected to be 1 40 | const project = await Project.create({ id: 1, title: 'bla' }); 41 | expect(project).to.exist; 42 | expect(project.title).to.equal('bla'); 43 | expect(project.id).to.equal(1); 44 | await this.sequelize.truncate(); 45 | const projects = await Project.findAll({}); 46 | expect(projects).to.exist; 47 | expect(projects).to.have.length(0); 48 | }); 49 | }); 50 | 51 | describe('sync', () => { 52 | // Edit reason: 53 | // Error message CRDB provides does not quote "bar" 54 | it('fails with incorrect database credentials (1)', async function () { 55 | this.sequelizeWithInvalidCredentials = new Sequelize( 56 | 'omg', 57 | 'bar', 58 | null, 59 | _.omit(this.sequelize.options, ['host']) 60 | ); 61 | 62 | const User2 = this.sequelizeWithInvalidCredentials.define('User', { 63 | name: DataTypes.STRING, 64 | bio: DataTypes.TEXT 65 | }); 66 | 67 | try { 68 | await User2.sync(); 69 | expect.fail(); 70 | } catch (err) { 71 | assert( 72 | [ 73 | 'fe_sendauth: no password supplied', 74 | 'role "bar" does not exist', 75 | 'FATAL: role "bar" does not exist', 76 | 'password authentication failed for user "bar"', 77 | // Added the error message generated by CRDB 78 | 'password authentication failed for user bar' 79 | ].includes(err.message.trim()) 80 | ); 81 | } 82 | }); 83 | }); 84 | 85 | describe('define', () => { 86 | describe('transaction', () => { 87 | beforeEach(async function () { 88 | const sequelize = await this.sequelize; 89 | this.sequelizeWithTransaction = sequelize; 90 | }); 91 | 92 | // Skip reason: 93 | // CRDB transactions have stronger locking restrictions so that a transaction 94 | // writing will block reads on the same object from a different transaction. 95 | it.skip('correctly handles multiple transactions', async function () { 96 | const TransactionTest = this.sequelizeWithTransaction.define( 97 | 'TransactionTest', 98 | { name: DataTypes.STRING }, 99 | { timestamps: false } 100 | ); 101 | const aliasesMapping = new Map([['_0', 'cnt']]); 102 | 103 | const count = async transaction => { 104 | const sql = this.sequelizeWithTransaction 105 | .getQueryInterface() 106 | .queryGenerator.selectQuery('TransactionTests', { 107 | attributes: [['count(*)', 'cnt']] 108 | }); 109 | 110 | const result = await this.sequelizeWithTransaction.query(sql, { 111 | plain: true, 112 | transaction, 113 | aliasesMapping 114 | }); 115 | 116 | return parseInt(result.cnt, 10); 117 | }; 118 | 119 | await TransactionTest.sync({ force: true }); 120 | const t1 = await this.sequelizeWithTransaction.transaction(); 121 | this.t1 = t1; 122 | await this.sequelizeWithTransaction.query( 123 | `INSERT INTO ${qq('TransactionTests')} (${qq( 124 | 'name' 125 | )}) VALUES ('foo');`, 126 | { transaction: t1 } 127 | ); 128 | const t2 = await this.sequelizeWithTransaction.transaction(); 129 | this.t2 = t2; 130 | await this.sequelizeWithTransaction.query( 131 | `INSERT INTO ${qq('TransactionTests')} (${qq( 132 | 'name' 133 | )}) VALUES ('bar');`, 134 | { transaction: t2 } 135 | ); 136 | await expect(count()).to.eventually.equal(0); 137 | await expect(count(this.t1)).to.eventually.equal(1); 138 | await expect(count(this.t2)).to.eventually.equal(1); 139 | await this.t2.rollback(); 140 | await expect(count()).to.eventually.equal(0); 141 | await this.t1.commit(); 142 | 143 | await expect(count()).to.eventually.equal(1); 144 | }); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /tests/support.js: -------------------------------------------------------------------------------- 1 | const { isDeepStrictEqual } = require('util'); 2 | const Sequelize = require('../source'); 3 | 4 | const Support = { 5 | createSequelizeInstance: function (options = {}) { 6 | return new Sequelize('sequelize_test', 'root', '', { 7 | dialect: 'postgres', 8 | port: process.env.COCKROACH_PORT || 26257, 9 | logging: console.log, 10 | typeValidation: true, 11 | minifyAliases: options.minifyAliases || false, 12 | dialectOptions: {cockroachdbTelemetryDisabled : true}, 13 | ...options 14 | }); 15 | }, 16 | 17 | isDeepEqualToOneOf: function (actual, expectedOptions) { 18 | return expectedOptions.some(expected => 19 | isDeepStrictEqual(actual, expected) 20 | ); 21 | }, 22 | 23 | getPoolMax: function () { 24 | // sequelize.config.pool.max default is 5. 25 | return 5; 26 | }, 27 | 28 | dropTestSchemas: async function (sequelize) { 29 | const schemas = await sequelize.showAllSchemas(); 30 | const schemasPromise = []; 31 | schemas.forEach(schema => { 32 | const schemaName = schema.name ? schema.name : schema; 33 | if (schemaName !== sequelize.config.database) { 34 | schemasPromise.push(sequelize.dropSchema(schemaName)); 35 | } 36 | }); 37 | 38 | await Promise.all(schemasPromise.map(p => p.catch(e => e))); 39 | } 40 | } 41 | 42 | Support.sequelize = Support.createSequelizeInstance(); 43 | 44 | module.exports = Support; 45 | -------------------------------------------------------------------------------- /tests/upsert_test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | require('./helper'); 16 | 17 | const { expect } = require('chai'); 18 | const { Sequelize, DataTypes } = require('../source'); 19 | 20 | describe('upsert', function () { 21 | it('supports CockroachDB', function () { 22 | expect(Sequelize.supportsCockroachDB).to.be.true; 23 | }); 24 | 25 | it('updates at most one row', async function () { 26 | const User = this.sequelize.define('user', { 27 | id: { 28 | type: DataTypes.INTEGER, 29 | primaryKey: true 30 | }, 31 | name: { 32 | type: DataTypes.STRING 33 | } 34 | }); 35 | 36 | const id1 = 1; 37 | const origName1 = 'original'; 38 | const updatedName1 = 'UPDATED'; 39 | 40 | const id2 = 2; 41 | const name2 = 'other'; 42 | 43 | await User.sync({ force: true }); 44 | const user1 = await User.create({ 45 | id: id1, 46 | name: origName1 47 | }); 48 | 49 | expect(user1.name).to.equal(origName1); 50 | expect(user1.updatedAt).to.equalTime(user1.createdAt); 51 | 52 | const user2 = await User.create({ 53 | id: id2, 54 | name: name2 55 | }); 56 | 57 | expect(user2.name).to.equal(name2); 58 | expect(user2.updatedAt).to.equalTime(user2.createdAt); 59 | 60 | await User.upsert({ 61 | id: id1, 62 | name: updatedName1 63 | }); 64 | 65 | const user1Again = await User.findByPk(id1); 66 | 67 | expect(user1Again.name).to.equal(updatedName1); 68 | expect(user1Again.updatedAt).afterTime(user1Again.createdAt); 69 | 70 | const user2Again = await User.findByPk(id2); 71 | 72 | // Verify that the other row is unmodified. 73 | expect(user2Again.name).to.equal(name2); 74 | expect(user2Again.updatedAt).to.equalTime(user2Again.createdAt); 75 | }); 76 | 77 | it('works with composite primary key', async function () { 78 | const Counter = this.sequelize.define('counter', { 79 | id: { 80 | type: DataTypes.INTEGER, 81 | primaryKey: true 82 | }, 83 | id2: { 84 | type: DataTypes.INTEGER, 85 | primaryKey: true 86 | }, 87 | count: { 88 | type: DataTypes.INTEGER 89 | } 90 | }); 91 | 92 | const id = 1000; 93 | const id2 = 2000; 94 | 95 | await Counter.sync({ force: true }); 96 | await Counter.create({ id: id, id2: id2, count: 1 }); 97 | await Counter.upsert({ id: id, id2: id2, count: 2 }); 98 | 99 | const counter = await Counter.findOne({ where: { id: id, id2: id2 } }); 100 | 101 | expect(counter.count).to.equal(2); 102 | expect(counter.updatedAt).afterTime(counter.createdAt); 103 | }); 104 | 105 | it('works with RETURNING', async function () { 106 | const User = this.sequelize.define('user', { name: DataTypes.STRING }); 107 | await User.sync({ force: true }); 108 | 109 | const { id } = await User.create({ name: 'Someone' }); 110 | 111 | const [userReturnedFromUpsert1] = await User.upsert( 112 | { id, name: 'Another Name' }, 113 | { returning: true } 114 | ); 115 | const user1 = await User.findOne(); 116 | 117 | expect(user1.name).to.equal('Another Name'); 118 | expect(userReturnedFromUpsert1.name).to.equal('Another Name'); 119 | 120 | const [userReturnedFromUpsert2] = await User.upsert( 121 | { id, name: 'Another Name 2' }, 122 | { returning: '*' } 123 | ); 124 | const user2 = await User.findOne(); 125 | 126 | expect(user2.name).to.equal('Another Name 2'); 127 | expect(userReturnedFromUpsert2.name).to.equal('Another Name 2'); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'sequelize-cockroachdb' { 2 | export * from 'sequelize'; 3 | } 4 | --------------------------------------------------------------------------------