├── .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 |
--------------------------------------------------------------------------------