From 62518ae1226f22b2f230afa615532c92f1544f01 Mon Sep 17 00:00:00 2001 From: AlexMesser Date: Tue, 12 Apr 2022 17:31:57 +0500 Subject: [PATCH] feat: Cloud Spanner support (#8730) * working on Cloud Spanner driver implementation * working on DDL synchronization * working on DDL synchronization * fixed failing test * working on VIEW implementation * fixed query parameters * lint * added transaction support; added streaming support; * fixed column types * fixes after merge * prettier * added support for generated columns * added escaping for distinct alias * working on generated columns * changed failing test * updated tests for Spanner; bugfixes; * updated tests for Spanner; bugfixes; * updated tests for Spanner; bugfixes; * fixed failing test * fixed failing test * fixing failing tests * fixing failing tests * fixing failing tests * added support for typeorm-generated uuid; fixed caching; * fixing failing tests * fixing failing tests * fixing failing tests * fixing failing tests * fixing failing tests * fixing failing tests * debugging failing test * debugging failing test * fixed bug in @PrimaryColumn decorator * fixed failing tests * fixed VIEW functionality; fixed failing tests; * updated docs --- .github/ISSUE_TEMPLATE/bug-report.md | 1 + .github/ISSUE_TEMPLATE/feature-request.md | 1 + README.md | 37 +- docker-compose.yml | 8 + docs/data-source-options.md | 2 +- docs/entities.md | 4 + package.json | 17 +- sample/sample1-simple-entity/entity/Post.ts | 2 +- src/cache/DbQueryResultCache.ts | 14 +- src/data-source/DataSourceOptions.ts | 2 + src/decorator/Index.ts | 1 + src/decorator/columns/PrimaryColumn.ts | 9 +- src/decorator/options/IndexOptions.ts | 9 + src/driver/DriverFactory.ts | 4 + .../SpannerConnectionCredentialsOptions.ts | 19 + .../spanner/SpannerConnectionOptions.ts | 147 ++ src/driver/spanner/SpannerDriver.ts | 794 +++++++ src/driver/spanner/SpannerQueryRunner.ts | 2103 +++++++++++++++++ src/driver/types/ColumnTypes.ts | 19 +- src/driver/types/DatabaseType.ts | 1 + src/entity-schema/EntitySchemaIndexOptions.ts | 9 + src/entity-schema/EntitySchemaTransformer.ts | 1 + src/logger/AdvancedConsoleLogger.ts | 2 +- src/metadata-args/IndexMetadataArgs.ts | 9 + src/metadata-builder/EntityMetadataBuilder.ts | 22 +- .../JunctionEntityMetadataBuilder.ts | 21 +- src/metadata/IndexMetadata.ts | 10 + src/platform/PlatformTools.ts | 7 +- src/query-builder/InsertQueryBuilder.ts | 19 +- src/query-builder/SelectQueryBuilder.ts | 28 +- src/query-builder/UpdateQueryBuilder.ts | 26 +- src/schema-builder/RdbmsSchemaBuilder.ts | 16 +- .../options/TableIndexOptions.ts | 9 + src/schema-builder/table/TableIndex.ts | 12 + test/__spanner-test/Dockerfile | 9 + test/__spanner-test/spanner-test.ts | 81 + test/__spanner-test/start_spanner.bash | 22 + .../functional/cache/custom-cache-provider.ts | 19 + .../update-insert/columns-update-insert.ts | 4 + .../spanner/column-types-spanner.ts | 160 ++ .../column-types/spanner/entity/Post.ts | 68 + .../spanner/entity/PostWithOptions.ts | 13 + .../spanner/entity/PostWithoutTypes.ts | 19 + .../driver/convert-to-entity/entity/Post.ts | 2 +- .../embedded-many-to-many-case1.ts | 2 +- .../embedded-many-to-many-case2.ts | 2 +- .../embedded-many-to-many-case3.ts | 4 +- .../embedded-many-to-many-case4.ts | 4 +- .../embedded-many-to-many-case5.ts | 4 +- .../embedded-many-to-one-case2.ts | 4 +- .../embedded-one-to-one.ts | 4 +- test/functional/entity-model/entity-model.ts | 18 +- test/functional/entity-model/entity/Post.ts | 4 +- .../basic/entity-schema-basic.ts | 1 + .../basic/entity/CategoryEntity.ts | 1 - .../entity-schema/basic/entity/PostEntity.ts | 1 - .../entity-schema/checks/checks-basic.ts | 71 +- .../entity-schema/checks/entity/Person.ts | 2 +- .../entity-schema/checks/entity/Person2.ts | 29 + .../exclusions/exclusions-basic.ts | 4 +- .../indices/basic/entity/Person.ts | 2 +- .../entity-schema/uniques/entity/Person.ts | 2 +- .../entity-schema/uniques/uniques-basic.ts | 3 +- .../entity-subscriber-transaction-flow.ts | 18 +- .../find-options/basic-usage/entity/Author.ts | 9 +- .../find-options/basic-usage/entity/Photo.ts | 9 +- .../find-options/basic-usage/entity/Post.ts | 4 +- .../find-options/basic-usage/entity/Tag.ts | 9 +- .../basic-usage/find-options-order.ts | 1 + .../basic-usage/find-options-test-utils.ts | 10 + .../basic-usage/find-options-where.ts | 30 +- .../bulk-insert-remove-optimization.ts | 4 +- .../entity/Category.ts | 4 +- .../entity/Post.ts | 4 +- .../cascades-example1/cascades-example1.ts | 5 + .../cascades-example1/entity/Photo.ts | 4 +- .../cascades-example1/entity/Profile.ts | 4 +- .../cascades/cascades-example1/entity/User.ts | 4 +- .../cascades-example2/cascades-example2.ts | 3 + .../persistence-entity-updation.ts | 9 + .../persistence-many-to-one-bi-directional.ts | 3 + ...persistence-many-to-one-uni-directional.ts | 3 + .../multi-primary-key.ts | 2 +- .../multi-primary-key/entity/Category.ts | 4 +- .../multi-primary-key/multi-primary-key.ts | 1 + .../one-to-many/entity/Category.ts | 4 +- .../persistence/one-to-many/entity/Post.ts | 8 +- .../one-to-many/persistence-one-to-many.ts | 13 + .../one-to-one/entity/AccessToken.ts | 2 +- .../persistence/one-to-one/entity/User.ts | 2 +- .../listeners/entity/Post.ts | 14 +- .../persistence-options-listeners.ts | 5 +- .../query-builder/cte/entity/foo.ts | 2 +- .../query-builder/cte/recursive-cte.ts | 3 + .../query-builder/cte/simple-cte.ts | 90 +- .../delete/query-builder-delete.ts | 3 +- .../entity-updation/entity-updation.ts | 5 + .../insert/query-builder-insert.ts | 12 +- .../query-builder/join/query-builder-joins.ts | 1 + .../locking/query-builder-locking.ts | 21 +- ...oad-relation-count-and-map-many-to-many.ts | 3 + ...load-relation-count-and-map-one-to-many.ts | 2 + .../many-to-many/multiple-pk/multiple-pk.ts | 12 + .../basic-functionality.ts | 1 + .../many-to-one/multiple-pk/multiple-pk.ts | 1 + .../basic-functionality.ts | 30 +- .../embedded-with-multiple-pk.ts | 4 + .../entity/Counters.ts | 2 +- .../one-to-many/embedded/embedded.ts | 6 + .../one-to-many/multiple-pk/entity/Post.ts | 2 +- .../one-to-many/multiple-pk/multiple-pk.ts | 31 +- .../basic-functionality.ts | 3 + .../one-to-one/multiple-pk/entity/Image.ts | 4 +- .../one-to-one/multiple-pk/multiple-pk.ts | 14 + test/functional/query-runner/add-column.ts | 86 +- test/functional/query-runner/change-column.ts | 38 +- .../query-runner/create-check-constraint.ts | 28 +- .../query-runner/create-foreign-key.ts | 23 +- test/functional/query-runner/create-index.ts | 19 +- .../query-runner/create-primary-key.ts | 8 +- test/functional/query-runner/create-table.ts | 255 +- test/functional/query-runner/drop-column.ts | 28 +- .../query-runner/drop-primary-key.ts | 6 +- test/functional/query-runner/entity/Book.ts | 3 +- .../functional/query-runner/entity/Faculty.ts | 4 +- test/functional/query-runner/entity/Photo.ts | 6 +- test/functional/query-runner/entity/Post.ts | 17 +- .../functional/query-runner/entity/Student.ts | 12 +- .../functional/query-runner/entity/Teacher.ts | 10 +- test/functional/query-runner/rename-table.ts | 30 +- test/functional/query-runner/stream.ts | 5 +- .../custom-referenced-column-name.ts | 12 + .../basic-eager-relations.ts | 5 + .../entity/Category.ts | 4 +- .../entity/Tag.ts | 4 +- .../entity/Setting.ts | 4 +- .../model-schema/QuestionSchema.ts | 4 +- .../basic-methods/model-schema/UserSchema.ts | 21 + .../basic-methods/repository-basic-methods.ts | 13 +- .../repository/basic-methods/schema/user.json | 2 +- .../repository/clear/entity/Post.ts | 2 +- .../repository/decrement/entity/Post.ts | 2 +- .../find-methods/repostiory-find-methods.ts | 5 +- .../find-methods/schema/UserEntity.ts | 4 +- .../find-options-locking.ts | 12 +- .../repository-find-operators.ts | 9 + .../find-options/repository-find-options.ts | 1 + .../repository/increment/entity/Post.ts | 2 +- test/functional/schema-builder/add-column.ts | 37 +- .../schema-builder/change-check-constraint.ts | 8 +- .../schema-builder/change-column.ts | 47 +- .../change-unique-constraint.ts | 18 +- .../functional/schema-builder/create-table.ts | 7 +- test/functional/schema-builder/entity/Post.ts | 3 +- .../schema-builder/update-primary-keys.ts | 16 +- .../basic-functionality/entity/Person.ts | 2 +- .../no-type-column/entity/Note.ts | 2 +- .../entity/Person.ts | 2 +- .../non-virtual-discriminator-column.ts | 1 + .../numeric-types/entity/Person.ts | 2 +- .../numeric-types/numeric-types.ts | 1 + .../relations/many-to-many/entity/Person.ts | 2 +- .../one-to-many-casecade-save/entity/Staff.ts | 2 +- .../relations/one-to-many/entity/Person.ts | 2 +- .../transaction-in-entity-manager.ts | 3 + .../single-query-runner.ts | 16 +- .../closure-table/closure-table.ts | 69 + .../materialized-path/materialized-path.ts | 57 + .../tree-tables/nested-set/nested-set.ts | 58 +- test/functional/uuid/spanner/entity/Post.ts | 14 + .../uuid/spanner/entity/Question.ts | 24 + test/functional/uuid/spanner/uuid-spanner.ts | 102 + .../general/view-entity-general.ts | 7 +- test/github-issues/1099/issue-1099.ts | 3 +- test/github-issues/1123/entity/Author.ts | 2 +- test/github-issues/1123/entity/Post.ts | 2 +- test/github-issues/1261/issue-1261.ts | 3 + test/github-issues/131/entity/Person.ts | 4 +- test/github-issues/1623/issue-1623.ts | 4 +- test/github-issues/1680/issue-1680.ts | 22 +- test/github-issues/175/issue-175.ts | 1 + test/github-issues/1780/entity/User.ts | 6 +- test/github-issues/2103/issue-2103.ts | 2 + .../github-issues/2201/entity/ver1/context.ts | 4 +- .../github-issues/2201/entity/ver2/context.ts | 4 +- test/github-issues/2201/issue-2201.ts | 2 +- test/github-issues/2298/issue-2298.ts | 5 + test/github-issues/2364/entity/dummy.ts | 2 +- test/github-issues/2364/entity/dummy2.ts | 2 +- test/github-issues/2364/issue-2364.ts | 3 + test/github-issues/2376/issue-2376.ts | 5 +- test/github-issues/2464/issue-2464.ts | 4 +- test/github-issues/2557/entity/dummy.ts | 2 +- test/github-issues/2800/entity/Vehicle.ts | 2 +- test/github-issues/2965/index.ts | 2 +- test/github-issues/2984/entity/issue/note.ts | 2 +- test/github-issues/2984/entity/wiki/note.ts | 2 +- test/github-issues/3047/entity/User.ts | 6 +- test/github-issues/3112/entity/User.ts | 3 +- test/github-issues/3803/entity/Post.ts | 4 +- test/github-issues/3803/issue-3803.ts | 3 +- test/github-issues/3946/issue-3946.ts | 3 + test/github-issues/3997/issue-3997.ts | 8 + test/github-issues/4277/entity/User.ts | 2 +- test/github-issues/4658/issue-4658.ts | 3 +- test/github-issues/495/entity/Item.ts | 4 +- test/github-issues/4980/issue-4980.ts | 1 + test/github-issues/5501/issue-5501.ts | 1 + test/github-issues/57/entity/AccessToken.ts | 2 +- test/github-issues/57/entity/User.ts | 2 +- test/github-issues/5762/entity/User.ts | 3 +- test/github-issues/58/issue-58.ts | 1 + .../6950/entity/post_with_null_2.entity.ts | 2 +- test/github-issues/70/issue-70.ts | 3 + test/github-issues/7065/entity/Contact.ts | 2 +- test/github-issues/71/entity/Artikel.ts | 2 +- test/github-issues/71/entity/Kollektion.ts | 2 +- test/github-issues/7109/issue-7109.ts | 8 +- test/github-issues/7415/issue-7415.ts | 5 +- test/github-issues/8076/issue-8076.ts | 2 + test/github-issues/8221/entity/Setting.ts | 4 +- test/github-issues/8221/issue-8221.ts | 6 +- .../8443/closure-table/closure-table.ts | 69 + .../materialized-path/materialized-path.ts | 69 + .../8443/nested-set/nested-set.ts | 62 +- test/github-issues/85/issue-85.ts | 6 + test/github-issues/8522/entity/Role.ts | 2 +- test/github-issues/8522/entity/User.ts | 2 +- test/github-issues/8690/entity/entities.ts | 6 +- test/github-issues/8723/entity/Photo.ts | 2 +- test/github-issues/8723/entity/User.ts | 2 +- .../escaping-function-parameter.ts | 1 + test/utils/test-utils.ts | 81 +- 233 files changed, 5466 insertions(+), 582 deletions(-) create mode 100644 src/driver/spanner/SpannerConnectionCredentialsOptions.ts create mode 100644 src/driver/spanner/SpannerConnectionOptions.ts create mode 100644 src/driver/spanner/SpannerDriver.ts create mode 100644 src/driver/spanner/SpannerQueryRunner.ts create mode 100644 test/__spanner-test/Dockerfile create mode 100644 test/__spanner-test/spanner-test.ts create mode 100644 test/__spanner-test/start_spanner.bash create mode 100644 test/functional/database-schema/column-types/spanner/column-types-spanner.ts create mode 100644 test/functional/database-schema/column-types/spanner/entity/Post.ts create mode 100644 test/functional/database-schema/column-types/spanner/entity/PostWithOptions.ts create mode 100644 test/functional/database-schema/column-types/spanner/entity/PostWithoutTypes.ts create mode 100644 test/functional/entity-schema/checks/entity/Person2.ts create mode 100644 test/functional/repository/basic-methods/model-schema/UserSchema.ts create mode 100644 test/functional/uuid/spanner/entity/Post.ts create mode 100644 test/functional/uuid/spanner/entity/Question.ts create mode 100644 test/functional/uuid/spanner/uuid-spanner.ts diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 3ba416e785..c0f95d5ddc 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -98,6 +98,7 @@ assignees: '' | `postgres` | no | | `react-native` | no | | `sap` | no | +| `spanner` | no | | `sqlite` | no | | `sqlite-abstract` | no | | `sqljs` | no | diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index bb506e5108..5c3fc3c9fb 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -76,6 +76,7 @@ assignees: '' | `postgres` | no | | `react-native` | no | | `sap` | no | +| `spanner` | no | | `sqlite` | no | | `sqlite-abstract` | no | | `sqljs` | no | diff --git a/README.md b/README.md index ef89502310..c9299c0736 100644 --- a/README.md +++ b/README.md @@ -214,12 +214,41 @@ await timber.remove() - for **SAP Hana** ``` - npm i @sap/hana-client - npm i hdb-pool + npm install @sap/hana-client + npm install hdb-pool ``` _SAP Hana support made possible by the sponsorship of [Neptune Software](https://www.neptune-software.com/)._ + - for **Google Cloud Spanner** + + ``` + npm install @google-cloud/spanner --save + ``` + + Provide authentication credentials to your application code + by setting the environment variable `GOOGLE_APPLICATION_CREDENTIALS`: + + ```shell + # Linux/macOS + export GOOGLE_APPLICATION_CREDENTIALS="KEY_PATH" + + # Windows + set GOOGLE_APPLICATION_CREDENTIALS=KEY_PATH + + # Replace KEY_PATH with the path of the JSON file that contains your service account key. + ``` + + To use Spanner with the emulator you should set `SPANNER_EMULATOR_HOST` environment variable: + + ```shell + # Linux/macOS + export SPANNER_EMULATOR_HOST=localhost:9010 + + # Windows + set SPANNER_EMULATOR_HOST=localhost:9010 + ``` + - for **MongoDB** (experimental) `npm install mongodb@^3.6.0 --save` @@ -255,7 +284,7 @@ npx typeorm init --name MyProject --database postgres ``` Where `name` is the name of your project and `database` is the database you'll use. -Database can be one of the following values: `mysql`, `mariadb`, `postgres`, `cockroachdb`, `sqlite`, `mssql`, `oracle`, `mongodb`, +Database can be one of the following values: `mysql`, `mariadb`, `postgres`, `cockroachdb`, `sqlite`, `mssql`, `sap`, `spanner`, `oracle`, `mongodb`, `cordova`, `react-native`, `expo`, `nativescript`. This command will generate a new project in the `MyProject` directory with the following files: @@ -553,7 +582,7 @@ AppDataSource.initialize() We are using Postgres in this example, but you can use any other supported database. To use another database, simply change the `type` in the options to the database type you are using: -`mysql`, `mariadb`, `postgres`, `cockroachdb`, `sqlite`, `mssql`, `oracle`, `cordova`, `nativescript`, `react-native`, +`mysql`, `mariadb`, `postgres`, `cockroachdb`, `sqlite`, `mssql`, `oracle`, `sap`, `spanner`, `cordova`, `nativescript`, `react-native`, `expo`, or `mongodb`. Also make sure to use your own host, port, username, password and database settings. diff --git a/docker-compose.yml b/docker-compose.yml index 47cb5408e9..179035b1b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -79,6 +79,14 @@ services: - default - typeorm + # google cloud spanner + spanner: + image: alexmesser/spanner-emulator + container_name: "typeorm-spanner" + ports: + - "9010:9010" + - "9020:9020" + # sap hana (works only on linux) # hanaexpress: # image: "store/saplabs/hanaexpress:2.00.040.00.20190729.1" diff --git a/docs/data-source-options.md b/docs/data-source-options.md index 742924bbd9..396f6332b8 100644 --- a/docs/data-source-options.md +++ b/docs/data-source-options.md @@ -25,7 +25,7 @@ Different RDBMS-es have their own specific options. - `type` - RDBMS type. You must specify what database engine you use. Possible values are: - "mysql", "postgres", "cockroachdb", "sap", "mariadb", "sqlite", "cordova", "react-native", "nativescript", "sqljs", "oracle", "mssql", "mongodb", "aurora-mysql", "aurora-postgres", "expo", "better-sqlite3", "capacitor". + "mysql", "postgres", "cockroachdb", "sap", "spanner", "mariadb", "sqlite", "cordova", "react-native", "nativescript", "sqljs", "oracle", "mssql", "mongodb", "aurora-mysql", "aurora-postgres", "expo", "better-sqlite3", "capacitor". This option is **required**. - `extra` - Extra options to be passed to the underlying driver. diff --git a/docs/entities.md b/docs/entities.md index b9229ba8d6..3906abda7f 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -337,6 +337,10 @@ or `timestamp with local time zone`, `interval year to month`, `interval day to second`, `bfile`, `blob`, `clob`, `nclob`, `rowid`, `urowid` +### Column types for `spanner` + +`bool`, `int64`, `float64`, `numeric`, `string`, `json`, `bytes`, `date`, `timestamp`, `array` + ### `enum` column type `enum` column type is supported by `postgres` and `mysql`. There are various possible column definitions: diff --git a/package.json b/package.json index 9724b06aca..82041cd2f9 100644 --- a/package.json +++ b/package.json @@ -74,12 +74,15 @@ "postgresql-orm", "mariadb", "mariadb-orm", + "spanner", "sqlite", "sqlite-orm", "sql-server", "sql-server-orm", "oracle", - "oracle-orm" + "oracle-orm", + "cloud-spanner", + "cloud-spanner-orm" ], "devDependencies": { "@types/app-root-path": "^1.2.4", @@ -121,6 +124,7 @@ "mysql2": "^2.2.5", "pg": "^8.5.1", "pg-query-stream": "^4.0.0", + "prettier": "^2.5.1", "redis": "^3.1.1", "remap-istanbul": "^0.13.0", "rimraf": "^3.0.2", @@ -131,10 +135,10 @@ "sqlite3": "^5.0.2", "ts-node": "^10.7.0", "typeorm-aurora-data-api-driver": "^2.0.0", - "typescript": "^4.6.2", - "prettier": "^2.5.1" + "typescript": "^4.6.2" }, "peerDependencies": { + "@google-cloud/spanner": "^5.18.0", "@sap/hana-client": "^2.11.14", "better-sqlite3": "^7.1.2", "hdb-pool": "^0.1.6", @@ -153,6 +157,9 @@ "typeorm-aurora-data-api-driver": "^2.0.0" }, "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, "@sap/hana-client": { "optional": true }, @@ -208,6 +215,7 @@ "buffer": "^6.0.3", "chalk": "^4.1.0", "cli-highlight": "^2.1.11", + "date-fns": "^2.28.0", "debug": "^4.3.3", "dotenv": "^16.0.0", "glob": "^7.2.0", @@ -218,8 +226,7 @@ "tslib": "^2.3.1", "uuid": "^8.3.2", "xml2js": "^0.4.23", - "yargs": "^17.3.1", - "date-fns": "^2.28.0" + "yargs": "^17.3.1" }, "scripts": { "test": "rimraf ./build && tsc && mocha --file ./build/compiled/test/utils/test-setup.js --bail --recursive --timeout 60000 ./build/compiled/test", diff --git a/sample/sample1-simple-entity/entity/Post.ts b/sample/sample1-simple-entity/entity/Post.ts index 5378851890..11bfdfa268 100644 --- a/sample/sample1-simple-entity/entity/Post.ts +++ b/sample/sample1-simple-entity/entity/Post.ts @@ -4,7 +4,7 @@ import { Generated } from "../../../src/decorator/Generated" @Entity("sample01_post") export class Post { - @PrimaryColumn("integer") + @PrimaryColumn() @Generated() id: number diff --git a/src/cache/DbQueryResultCache.ts b/src/cache/DbQueryResultCache.ts index 82dcef0c63..005ce693e7 100644 --- a/src/cache/DbQueryResultCache.ts +++ b/src/cache/DbQueryResultCache.ts @@ -5,6 +5,7 @@ import { QueryRunner } from "../query-runner/QueryRunner" import { Table } from "../schema-builder/table/Table" import { QueryResultCache } from "./QueryResultCache" import { QueryResultCacheOptions } from "./QueryResultCacheOptions" +import { v4 as uuidv4 } from "uuid" /** * Caches query result into current database, into separate table called "query-result-cache". @@ -80,7 +81,10 @@ export class DbQueryResultCache implements QueryResultCache { type: driver.normalizeType({ type: driver.mappedDataTypes.cacheId, }), - generationStrategy: "increment", + generationStrategy: + driver.options.type === "spanner" + ? "uuid" + : "increment", isGenerated: true, }, { @@ -256,6 +260,14 @@ export class DbQueryResultCache implements QueryResultCache { await qb.execute() } else { + // Spanner does not support auto-generated columns + if ( + this.connection.driver.options.type === "spanner" && + !insertedValues.id + ) { + insertedValues.id = uuidv4() + } + // otherwise insert await queryRunner.manager .createQueryBuilder() diff --git a/src/data-source/DataSourceOptions.ts b/src/data-source/DataSourceOptions.ts index 7fe181d7c3..1b4a589f53 100644 --- a/src/data-source/DataSourceOptions.ts +++ b/src/data-source/DataSourceOptions.ts @@ -15,6 +15,7 @@ import { SapConnectionOptions } from "../driver/sap/SapConnectionOptions" import { AuroraPostgresConnectionOptions } from "../driver/aurora-postgres/AuroraPostgresConnectionOptions" import { BetterSqlite3ConnectionOptions } from "../driver/better-sqlite3/BetterSqlite3ConnectionOptions" import { CapacitorConnectionOptions } from "../driver/capacitor/CapacitorConnectionOptions" +import { SpannerConnectionOptions } from "../driver/spanner/SpannerConnectionOptions" /** * DataSourceOptions is an interface with settings and options for specific DataSource. @@ -37,3 +38,4 @@ export type DataSourceOptions = | ExpoConnectionOptions | BetterSqlite3ConnectionOptions | CapacitorConnectionOptions + | SpannerConnectionOptions diff --git a/src/decorator/Index.ts b/src/decorator/Index.ts index 9523be15ab..9e36cdad5f 100644 --- a/src/decorator/Index.ts +++ b/src/decorator/Index.ts @@ -135,6 +135,7 @@ export function Index( unique: options && options.unique ? true : false, spatial: options && options.spatial ? true : false, fulltext: options && options.fulltext ? true : false, + nullFiltered: options && options.nullFiltered ? true : false, parser: options ? options.parser : undefined, sparse: options && options.sparse ? true : false, background: options && options.background ? true : false, diff --git a/src/decorator/columns/PrimaryColumn.ts b/src/decorator/columns/PrimaryColumn.ts index 9356f9b5f0..83d8a36595 100644 --- a/src/decorator/columns/PrimaryColumn.ts +++ b/src/decorator/columns/PrimaryColumn.ts @@ -41,8 +41,13 @@ export function PrimaryColumn( return function (object: Object, propertyName: string) { // normalize parameters let type: ColumnType | undefined - if (typeof typeOrOptions === "string") { - type = typeOrOptions + if ( + typeof typeOrOptions === "string" || + typeOrOptions === String || + typeOrOptions === Boolean || + typeOrOptions === Number + ) { + type = typeOrOptions as ColumnType } else { options = Object.assign({}, typeOrOptions) } diff --git a/src/decorator/options/IndexOptions.ts b/src/decorator/options/IndexOptions.ts index bb22bc138b..6f319ef586 100644 --- a/src/decorator/options/IndexOptions.ts +++ b/src/decorator/options/IndexOptions.ts @@ -19,6 +19,15 @@ export interface IndexOptions { */ fulltext?: boolean + /** + * NULL_FILTERED indexes are particularly useful for indexing sparse columns, where most rows contain a NULL value. + * In these cases, the NULL_FILTERED index can be considerably smaller and more efficient to maintain than + * a normal index that includes NULL values. + * + * Works only in Spanner. + */ + nullFiltered?: boolean + /** * Fulltext parser. * Works only in MySQL. diff --git a/src/driver/DriverFactory.ts b/src/driver/DriverFactory.ts index 0070ca566e..65bd113a24 100644 --- a/src/driver/DriverFactory.ts +++ b/src/driver/DriverFactory.ts @@ -18,6 +18,7 @@ import { DataSource } from "../data-source/DataSource" import { SapDriver } from "./sap/SapDriver" import { BetterSqlite3Driver } from "./better-sqlite3/BetterSqlite3Driver" import { CapacitorDriver } from "./capacitor/CapacitorDriver" +import { SpannerDriver } from "./spanner/SpannerDriver" /** * Helps to create drivers. @@ -65,6 +66,8 @@ export class DriverFactory { return new AuroraPostgresDriver(connection) case "capacitor": return new CapacitorDriver(connection) + case "spanner": + return new SpannerDriver(connection) default: throw new MissingDriverError(type, [ "aurora-mysql", @@ -85,6 +88,7 @@ export class DriverFactory { "sap", "sqlite", "sqljs", + "spanner", ]) } } diff --git a/src/driver/spanner/SpannerConnectionCredentialsOptions.ts b/src/driver/spanner/SpannerConnectionCredentialsOptions.ts new file mode 100644 index 0000000000..473d1f6236 --- /dev/null +++ b/src/driver/spanner/SpannerConnectionCredentialsOptions.ts @@ -0,0 +1,19 @@ +/** + * Spanner specific connection credential options. + */ +export interface SpannerConnectionCredentialsOptions { + /** + * Connection url where perform connection to. + */ + readonly instanceId?: string + + /** + * Database host. + */ + readonly projectId?: string + + /** + * Database host port. + */ + readonly databaseId?: string +} diff --git a/src/driver/spanner/SpannerConnectionOptions.ts b/src/driver/spanner/SpannerConnectionOptions.ts new file mode 100644 index 0000000000..c669f7bd0c --- /dev/null +++ b/src/driver/spanner/SpannerConnectionOptions.ts @@ -0,0 +1,147 @@ +import { BaseConnectionOptions } from "../../connection/BaseConnectionOptions" +import { SpannerConnectionCredentialsOptions } from "./SpannerConnectionCredentialsOptions" + +/** + * Spanner specific connection options. + */ +export interface SpannerConnectionOptions + extends BaseConnectionOptions, + SpannerConnectionCredentialsOptions { + /** + * Database type. + */ + readonly type: "spanner" + + /** + * The driver object + * This defaults to require("@google-cloud/spanner"). + */ + readonly driver?: any + + // todo + readonly database?: string + + // todo + readonly schema?: string + + /** + * The charset for the connection. This is called "collation" in the SQL-level of MySQL (like utf8_general_ci). + * If a SQL-level charset is specified (like utf8mb4) then the default collation for that charset is used. + * Default: 'UTF8_GENERAL_CI' + */ + readonly charset?: string + + /** + * The timezone configured on the MySQL server. + * This is used to type cast server date/time values to JavaScript Date object and vice versa. + * This can be 'local', 'Z', or an offset in the form +HH:MM or -HH:MM. (Default: 'local') + */ + readonly timezone?: string + + /** + * The milliseconds before a timeout occurs during the initial connection to the MySQL server. (Default: 10000) + */ + readonly connectTimeout?: number + + /** + * The milliseconds before a timeout occurs during the initial connection to the MySQL server. (Default: 10000) + * This difference between connectTimeout and acquireTimeout is subtle and is described in the mysqljs/mysql docs + * https://github.com/mysqljs/mysql/tree/master#pool-options + */ + readonly acquireTimeout?: number + + /** + * Allow connecting to MySQL instances that ask for the old (insecure) authentication method. (Default: false) + */ + readonly insecureAuth?: boolean + + /** + * When dealing with big numbers (BIGINT and DECIMAL columns) in the database, you should enable this option (Default: false) + */ + readonly supportBigNumbers?: boolean + + /** + * Enabling both supportBigNumbers and bigNumberStrings forces big numbers (BIGINT and DECIMAL columns) to be always + * returned as JavaScript String objects (Default: false). Enabling supportBigNumbers but leaving bigNumberStrings + * disabled will return big numbers as String objects only when they cannot be accurately represented with + * [JavaScript Number objects](http://ecma262-5.com/ELS5_HTML.htm#Section_8.5) (which happens when they exceed the [-2^53, +2^53] range), + * otherwise they will be returned as Number objects. This option is ignored if supportBigNumbers is disabled. + */ + readonly bigNumberStrings?: boolean + + /** + * Force date types (TIMESTAMP, DATETIME, DATE) to be returned as strings rather then inflated into JavaScript Date objects. + * Can be true/false or an array of type names to keep as strings. + */ + readonly dateStrings?: boolean | string[] + + /** + * Prints protocol details to stdout. Can be true/false or an array of packet type names that should be printed. + * (Default: false) + */ + readonly debug?: boolean | string[] + + /** + * Generates stack traces on Error to include call site of library entrance ("long stack traces"). + * Slight performance penalty for most calls. (Default: true) + */ + readonly trace?: boolean + + /** + * Allow multiple mysql statements per query. Be careful with this, it could increase the scope of SQL injection attacks. + * (Default: false) + */ + readonly multipleStatements?: boolean + + /** + * Use spatial functions like GeomFromText and AsText which are removed in MySQL 8. + * (Default: true) + */ + readonly legacySpatialSupport?: boolean + + /** + * List of connection flags to use other than the default ones. It is also possible to blacklist default ones. + * For more information, check https://github.com/mysqljs/mysql#connection-flags. + */ + readonly flags?: string[] + + /** + * Replication setup. + */ + readonly replication?: { + /** + * Master server used by orm to perform writes. + */ + readonly master: SpannerConnectionCredentialsOptions + + /** + * List of read-from severs (slaves). + */ + readonly slaves: SpannerConnectionCredentialsOptions[] + + /** + * If true, PoolCluster will attempt to reconnect when connection fails. (Default: true) + */ + readonly canRetry?: boolean + + /** + * If connection fails, node's errorCount increases. + * When errorCount is greater than removeNodeErrorCount, remove a node in the PoolCluster. (Default: 5) + */ + readonly removeNodeErrorCount?: number + + /** + * If connection fails, specifies the number of milliseconds before another connection attempt will be made. + * If set to 0, then node will be removed instead and never re-used. (Default: 0) + */ + readonly restoreNodeTimeout?: number + + /** + * Determines how slaves are selected: + * RR: Select one alternately (Round-Robin). + * RANDOM: Select the node by random function. + * ORDER: Select the first node available unconditionally. + */ + readonly selector?: "RR" | "RANDOM" | "ORDER" + } +} diff --git a/src/driver/spanner/SpannerDriver.ts b/src/driver/spanner/SpannerDriver.ts new file mode 100644 index 0000000000..c6f341ac07 --- /dev/null +++ b/src/driver/spanner/SpannerDriver.ts @@ -0,0 +1,794 @@ +import { Driver, ReturningType } from "../Driver" +import { DriverPackageNotInstalledError } from "../../error/DriverPackageNotInstalledError" +import { SpannerQueryRunner } from "./SpannerQueryRunner" +import { ObjectLiteral } from "../../common/ObjectLiteral" +import { ColumnMetadata } from "../../metadata/ColumnMetadata" +import { DateUtils } from "../../util/DateUtils" +import { PlatformTools } from "../../platform/PlatformTools" +import { Connection } from "../../connection/Connection" +import { RdbmsSchemaBuilder } from "../../schema-builder/RdbmsSchemaBuilder" +import { SpannerConnectionOptions } from "./SpannerConnectionOptions" +import { MappedColumnTypes } from "../types/MappedColumnTypes" +import { ColumnType } from "../types/ColumnTypes" +import { DataTypeDefaults } from "../types/DataTypeDefaults" +import { TableColumn } from "../../schema-builder/table/TableColumn" +import { EntityMetadata } from "../../metadata/EntityMetadata" +import { OrmUtils } from "../../util/OrmUtils" +import { ApplyValueTransformers } from "../../util/ApplyValueTransformers" +import { ReplicationMode } from "../types/ReplicationMode" +import { Table } from "../../schema-builder/table/Table" +import { View } from "../../schema-builder/view/View" +import { TableForeignKey } from "../../schema-builder/table/TableForeignKey" +import { CteCapabilities } from "../types/CteCapabilities" + +/** + * Organizes communication with Spanner DBMS. + */ +export class SpannerDriver implements Driver { + // ------------------------------------------------------------------------- + // Public Properties + // ------------------------------------------------------------------------- + + /** + * Connection used by driver. + */ + connection: Connection + + /** + * Cloud Spanner underlying library. + */ + spanner: any + + /** + * Cloud Spanner instance. + */ + instance: any + + /** + * Cloud Spanner database. + */ + instanceDatabase: any + + /** + * Database name. + */ + database?: string + + // ------------------------------------------------------------------------- + // Public Implemented Properties + // ------------------------------------------------------------------------- + + /** + * Connection options. + */ + options: SpannerConnectionOptions + + /** + * Indicates if replication is enabled. + */ + isReplicated: boolean = false + + /** + * Indicates if tree tables are supported by this driver. + */ + treeSupport = true + + /** + * Represent transaction support by this driver + */ + transactionSupport = "none" as const + + /** + * Gets list of supported column data types by a driver. + * + * @see https://cloud.google.com/spanner/docs/reference/standard-sql/data-types + */ + supportedDataTypes: ColumnType[] = [ + "bool", + "int64", + "float64", + "numeric", + "string", + "json", + "bytes", + "date", + "timestamp", + "array", + ] + + /** + * Returns type of upsert supported by driver if any + */ + readonly supportedUpsertType = undefined + + /** + * Gets list of spatial column data types. + */ + spatialTypes: ColumnType[] = [] + + /** + * Gets list of column data types that support length by a driver. + */ + withLengthColumnTypes: ColumnType[] = ["string", "bytes"] + + /** + * Gets list of column data types that support length by a driver. + */ + withWidthColumnTypes: ColumnType[] = [] + + /** + * Gets list of column data types that support precision by a driver. + */ + withPrecisionColumnTypes: ColumnType[] = [] + + /** + * Gets list of column data types that supports scale by a driver. + */ + withScaleColumnTypes: ColumnType[] = [] + + /** + * ORM has special columns and we need to know what database column types should be for those columns. + * Column types are driver dependant. + */ + mappedDataTypes: MappedColumnTypes = { + createDate: "timestamp", + createDateDefault: "", + updateDate: "timestamp", + updateDateDefault: "", + deleteDate: "timestamp", + deleteDateNullable: true, + version: "int64", + treeLevel: "int64", + migrationId: "int64", + migrationName: "string", + migrationTimestamp: "int64", + cacheId: "string", + cacheIdentifier: "string", + cacheTime: "int64", + cacheDuration: "int64", + cacheQuery: "string", + cacheResult: "string", + metadataType: "string", + metadataDatabase: "string", + metadataSchema: "string", + metadataTable: "string", + metadataName: "string", + metadataValue: "string", + } + + /** + * Default values of length, precision and scale depends on column data type. + * Used in the cases when length/precision/scale is not specified by user. + */ + dataTypeDefaults: DataTypeDefaults = {} + + /** + * Max length allowed by MySQL for aliases. + * @see https://dev.mysql.com/doc/refman/5.5/en/identifiers.html + */ + maxAliasLength = 63 + + cteCapabilities: CteCapabilities = { + enabled: true, + } + + /** + * Supported returning types + */ + private readonly _isReturningSqlSupported: Record = + { + delete: false, + insert: false, + update: false, + } + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(connection: Connection) { + this.connection = connection + this.options = connection.options as SpannerConnectionOptions + this.isReplicated = this.options.replication ? true : false + + // load mysql package + this.loadDependencies() + } + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + + /** + * Performs connection to the database. + */ + async connect(): Promise { + this.instance = this.spanner.instance(this.options.instanceId) + this.instanceDatabase = this.instance.database(this.options.databaseId) + } + + /** + * Makes any action after connection (e.g. create extensions in Postgres driver). + */ + afterConnect(): Promise { + return Promise.resolve() + } + + /** + * Closes connection with the database. + */ + async disconnect(): Promise { + this.instanceDatabase.close() + } + + /** + * Creates a schema builder used to build and sync a schema. + */ + createSchemaBuilder() { + return new RdbmsSchemaBuilder(this.connection) + } + + /** + * Creates a query runner used to execute database queries. + */ + createQueryRunner(mode: ReplicationMode) { + return new SpannerQueryRunner(this, mode) + } + + /** + * Replaces parameters in the given sql with special escaping character + * and an array of parameter names to be passed to a query. + */ + escapeQueryWithParameters( + sql: string, + parameters: ObjectLiteral, + nativeParameters: ObjectLiteral, + ): [string, any[]] { + const escapedParameters: any[] = Object.keys(nativeParameters).map( + (key) => nativeParameters[key], + ) + if (!parameters || !Object.keys(parameters).length) + return [sql, escapedParameters] + + sql = sql.replace( + /:(\.\.\.)?([A-Za-z0-9_.]+)/g, + (full, isArray: string, key: string): string => { + if (!parameters.hasOwnProperty(key)) { + return full + } + + let value: any = parameters[key] + + if (value === null) { + return full + } + + if (isArray) { + return value + .map((v: any) => { + escapedParameters.push(v) + return this.createParameter( + key, + escapedParameters.length - 1, + ) + }) + .join(", ") + } + + if (value instanceof Function) { + return value() + } + escapedParameters.push(value) + return this.createParameter(key, escapedParameters.length - 1) + }, + ) // todo: make replace only in value statements, otherwise problems + + sql = sql.replace( + /([ ]+)?=([ ]+)?:(\.\.\.)?([A-Za-z0-9_.]+)/g, + ( + full, + emptySpaceBefore: string, + emptySpaceAfter: string, + isArray: string, + key: string, + ): string => { + if (!parameters.hasOwnProperty(key)) { + return full + } + + let value: any = parameters[key] + if (value === null) { + return " IS NULL" + } + + return full + }, + ) + return [sql, escapedParameters] + } + + /** + * Escapes a column name. + */ + escape(columnName: string): string { + return `\`${columnName}\`` + } + + /** + * Build full table name with database name, schema name and table name. + * E.g. myDB.mySchema.myTable + */ + buildTableName( + tableName: string, + schema?: string, + database?: string, + ): string { + let tablePath = [tableName] + + if (database) { + tablePath.unshift(database) + } + + return tablePath.join(".") + } + + /** + * Parse a target table name or other types and return a normalized table definition. + */ + parseTableName( + target: EntityMetadata | Table | View | TableForeignKey | string, + ): { database?: string; schema?: string; tableName: string } { + const driverDatabase = this.database + const driverSchema = undefined + + if (target instanceof Table || target instanceof View) { + const parsed = this.parseTableName(target.name) + + return { + database: target.database || parsed.database || driverDatabase, + schema: target.schema || parsed.schema || driverSchema, + tableName: parsed.tableName, + } + } + + if (target instanceof TableForeignKey) { + const parsed = this.parseTableName(target.referencedTableName) + + return { + database: + target.referencedDatabase || + parsed.database || + driverDatabase, + schema: + target.referencedSchema || parsed.schema || driverSchema, + tableName: parsed.tableName, + } + } + + if (target instanceof EntityMetadata) { + // EntityMetadata tableName is never a path + + return { + database: target.database || driverDatabase, + schema: target.schema || driverSchema, + tableName: target.tableName, + } + } + + const parts = target.split(".") + + return { + database: + (parts.length > 1 ? parts[0] : undefined) || driverDatabase, + schema: driverSchema, + tableName: parts.length > 1 ? parts[1] : parts[0], + } + } + + /** + * Prepares given value to a value to be persisted, based on its column type and metadata. + */ + preparePersistentValue(value: any, columnMetadata: ColumnMetadata): any { + if (columnMetadata.transformer) + value = ApplyValueTransformers.transformTo( + columnMetadata.transformer, + value, + ) + + if (value === null || value === undefined) return value + + if (columnMetadata.type === "numeric") { + const lib = this.options.driver || PlatformTools.load("spanner") + return lib.Spanner.numeric(value) + } else if (columnMetadata.type === "date") { + return DateUtils.mixedDateToDateString(value) + } else if (columnMetadata.type === "json") { + return value + } else if ( + columnMetadata.type === "timestamp" || + columnMetadata.type === Date + ) { + return DateUtils.mixedDateToDate(value) + } + + return value + } + + /** + * Prepares given value to a value to be persisted, based on its column type or metadata. + */ + prepareHydratedValue(value: any, columnMetadata: ColumnMetadata): any { + if (value === null || value === undefined) + return columnMetadata.transformer + ? ApplyValueTransformers.transformFrom( + columnMetadata.transformer, + value, + ) + : value + + if (columnMetadata.type === Boolean || columnMetadata.type === "bool") { + value = value ? true : false + } else if ( + columnMetadata.type === "timestamp" || + columnMetadata.type === Date + ) { + value = new Date(value) + } else if (columnMetadata.type === "numeric") { + value = value.value + } else if (columnMetadata.type === "date") { + value = DateUtils.mixedDateToDateString(value) + } else if (columnMetadata.type === "json") { + value = typeof value === "string" ? JSON.parse(value) : value + } + + if (columnMetadata.transformer) + value = ApplyValueTransformers.transformFrom( + columnMetadata.transformer, + value, + ) + + return value + } + + /** + * Creates a database type from a given column metadata. + */ + normalizeType(column: { + type: ColumnType + length?: number | string + precision?: number | null + scale?: number + }): string { + if (column.type === Number) { + return "int64" + } else if (column.type === String || column.type === "uuid") { + return "string" + } else if (column.type === Date) { + return "timestamp" + } else if ((column.type as any) === Buffer) { + return "bytes" + } else if (column.type === Boolean) { + return "bool" + } else { + return (column.type as string) || "" + } + } + + /** + * Normalizes "default" value of the column. + * + * Spanner does not support default values. + */ + normalizeDefault(columnMetadata: ColumnMetadata): string | undefined { + return columnMetadata.default === "" + ? `"${columnMetadata.default}"` + : `${columnMetadata.default}` + } + + /** + * Normalizes "isUnique" value of the column. + */ + normalizeIsUnique(column: ColumnMetadata): boolean { + return column.entityMetadata.indices.some( + (idx) => + idx.isUnique && + idx.columns.length === 1 && + idx.columns[0] === column, + ) + } + + /** + * Returns default column lengths, which is required on column creation. + */ + getColumnLength(column: ColumnMetadata | TableColumn): string { + if (column.length) return column.length.toString() + if (column.generationStrategy === "uuid") return "36" + + switch (column.type) { + case String: + case "string": + case "bytes": + return "max" + default: + return "" + } + } + + /** + * Creates column type definition including length, precision and scale + */ + createFullType(column: TableColumn): string { + let type = column.type + + // used 'getColumnLength()' method, because Spanner requires column length for `string` and `bytes` data types + if (this.getColumnLength(column)) { + type += `(${this.getColumnLength(column)})` + } else if (column.width) { + type += `(${column.width})` + } else if ( + column.precision !== null && + column.precision !== undefined && + column.scale !== null && + column.scale !== undefined + ) { + type += `(${column.precision},${column.scale})` + } else if ( + column.precision !== null && + column.precision !== undefined + ) { + type += `(${column.precision})` + } + + if (column.isArray) type = `array<${type}>` + + return type + } + + /** + * Obtains a new database connection to a master server. + * Used for replication. + * If replication is not setup then returns default connection's database connection. + */ + obtainMasterConnection(): Promise { + return this.instanceDatabase + } + + /** + * Obtains a new database connection to a slave server. + * Used for replication. + * If replication is not setup then returns master (default) connection's database connection. + */ + obtainSlaveConnection(): Promise { + return this.instanceDatabase + } + + /** + * Creates generated map of values generated or returned by database after INSERT query. + */ + createGeneratedMap( + metadata: EntityMetadata, + insertResult: any, + entityIndex: number, + ) { + if (!insertResult) { + return undefined + } + + if (insertResult.insertId === undefined) { + return Object.keys(insertResult).reduce((map, key) => { + const column = metadata.findColumnWithDatabaseName(key) + if (column) { + OrmUtils.mergeDeep( + map, + column.createValueMap(insertResult[key]), + ) + // OrmUtils.mergeDeep(map, column.createValueMap(this.prepareHydratedValue(insertResult[key], column))); // TODO: probably should be like there, but fails on enums, fix later + } + return map + }, {} as ObjectLiteral) + } + + const generatedMap = metadata.generatedColumns.reduce( + (map, generatedColumn) => { + let value: any + if ( + generatedColumn.generationStrategy === "increment" && + insertResult.insertId + ) { + // NOTE: When multiple rows is inserted by a single INSERT statement, + // `insertId` is the value generated for the first inserted row only. + value = insertResult.insertId + entityIndex + // } else if (generatedColumn.generationStrategy === "uuid") { + // console.log("getting db value:", generatedColumn.databaseName); + // value = generatedColumn.getEntityValue(uuidMap); + } + + return OrmUtils.mergeDeep( + map, + generatedColumn.createValueMap(value), + ) + }, + {} as ObjectLiteral, + ) + + return Object.keys(generatedMap).length > 0 ? generatedMap : undefined + } + + /** + * Differentiate columns of this table and columns from the given column metadatas columns + * and returns only changed. + */ + findChangedColumns( + tableColumns: TableColumn[], + columnMetadatas: ColumnMetadata[], + ): ColumnMetadata[] { + return columnMetadatas.filter((columnMetadata) => { + const tableColumn = tableColumns.find( + (c) => c.name === columnMetadata.databaseName, + ) + if (!tableColumn) return false // we don't need new columns, we only need exist and changed + + const isColumnChanged = + tableColumn.name !== columnMetadata.databaseName || + tableColumn.type !== this.normalizeType(columnMetadata) || + tableColumn.length !== this.getColumnLength(columnMetadata) || + tableColumn.asExpression !== columnMetadata.asExpression || + tableColumn.generatedType !== columnMetadata.generatedType || + tableColumn.isPrimary !== columnMetadata.isPrimary || + tableColumn.isNullable !== columnMetadata.isNullable || + tableColumn.isUnique !== this.normalizeIsUnique(columnMetadata) + + // DEBUG SECTION + if (isColumnChanged) { + console.log("table:", columnMetadata.entityMetadata.tableName) + console.log( + "name:", + tableColumn.name, + columnMetadata.databaseName, + ) + console.log( + "type:", + tableColumn.type, + this.normalizeType(columnMetadata), + ) + console.log( + "length:", + tableColumn.length, + this.getColumnLength(columnMetadata), + ) + console.log( + "asExpression:", + tableColumn.asExpression, + columnMetadata.asExpression, + ) + console.log( + "generatedType:", + tableColumn.generatedType, + columnMetadata.generatedType, + ) + console.log( + "isPrimary:", + tableColumn.isPrimary, + columnMetadata.isPrimary, + ) + console.log( + "isNullable:", + tableColumn.isNullable, + columnMetadata.isNullable, + ) + console.log( + "isUnique:", + tableColumn.isUnique, + this.normalizeIsUnique(columnMetadata), + ) + console.log("==========================================") + } + + return isColumnChanged + }) + } + + /** + * Returns true if driver supports RETURNING / OUTPUT statement. + */ + isReturningSqlSupported(returningType: ReturningType): boolean { + return this._isReturningSqlSupported[returningType] + } + + /** + * Returns true if driver supports uuid values generation on its own. + */ + isUUIDGenerationSupported(): boolean { + return false + } + + /** + * Returns true if driver supports fulltext indices. + */ + isFullTextColumnTypeSupported(): boolean { + return false + } + + /** + * Creates an escaped parameter. + */ + createParameter(parameterName: string, index: number): string { + return "@param" + index + } + + // ------------------------------------------------------------------------- + // Protected Methods + // ------------------------------------------------------------------------- + + /** + * Loads all driver dependencies. + */ + protected loadDependencies(): void { + try { + const lib = this.options.driver || PlatformTools.load("spanner") + this.spanner = new lib.Spanner({ + projectId: this.options.projectId, + }) + } catch (e) { + console.error(e) + throw new DriverPackageNotInstalledError( + "Spanner", + "@google-cloud/spanner", + ) + } + } + + /** + * Checks if "DEFAULT" values in the column metadata and in the database are equal. + */ + protected compareDefaultValues( + columnMetadataValue: string | undefined, + databaseValue: string | undefined, + ): boolean { + if ( + typeof columnMetadataValue === "string" && + typeof databaseValue === "string" + ) { + // we need to cut out "'" because in mysql we can understand returned value is a string or a function + // as result compare cannot understand if default is really changed or not + columnMetadataValue = columnMetadataValue.replace(/^'+|'+$/g, "") + databaseValue = databaseValue.replace(/^'+|'+$/g, "") + } + + return columnMetadataValue === databaseValue + } + + /** + * If parameter is a datetime function, e.g. "CURRENT_TIMESTAMP", normalizes it. + * Otherwise returns original input. + */ + protected normalizeDatetimeFunction(value?: string) { + if (!value) return value + + // check if input is datetime function + const isDatetimeFunction = + value.toUpperCase().indexOf("CURRENT_TIMESTAMP") !== -1 || + value.toUpperCase().indexOf("NOW") !== -1 + + if (isDatetimeFunction) { + // extract precision, e.g. "(3)" + const precision = value.match(/\(\d+\)/) + return precision + ? `CURRENT_TIMESTAMP${precision[0]}` + : "CURRENT_TIMESTAMP" + } else { + return value + } + } + + /** + * Escapes a given comment. + */ + protected escapeComment(comment?: string) { + if (!comment) return comment + + comment = comment.replace(/\u0000/g, "") // Null bytes aren't allowed in comments + + return comment + } +} diff --git a/src/driver/spanner/SpannerQueryRunner.ts b/src/driver/spanner/SpannerQueryRunner.ts new file mode 100644 index 0000000000..bc5a29d1e5 --- /dev/null +++ b/src/driver/spanner/SpannerQueryRunner.ts @@ -0,0 +1,2103 @@ +import { ObjectLiteral } from "../../common/ObjectLiteral" +import { QueryFailedError } from "../../error/QueryFailedError" +import { QueryRunnerAlreadyReleasedError } from "../../error/QueryRunnerAlreadyReleasedError" +import { TransactionNotStartedError } from "../../error/TransactionNotStartedError" +import { ColumnType } from "../types/ColumnTypes" +import { ReadStream } from "../../platform/PlatformTools" +import { BaseQueryRunner } from "../../query-runner/BaseQueryRunner" +import { QueryRunner } from "../../query-runner/QueryRunner" +import { TableIndexOptions } from "../../schema-builder/options/TableIndexOptions" +import { Table } from "../../schema-builder/table/Table" +import { TableCheck } from "../../schema-builder/table/TableCheck" +import { TableColumn } from "../../schema-builder/table/TableColumn" +import { TableExclusion } from "../../schema-builder/table/TableExclusion" +import { TableForeignKey } from "../../schema-builder/table/TableForeignKey" +import { TableIndex } from "../../schema-builder/table/TableIndex" +import { TableUnique } from "../../schema-builder/table/TableUnique" +import { View } from "../../schema-builder/view/View" +import { Broadcaster } from "../../subscriber/Broadcaster" +import { OrmUtils } from "../../util/OrmUtils" +import { Query } from "../Query" +import { IsolationLevel } from "../types/IsolationLevel" +import { ReplicationMode } from "../types/ReplicationMode" +import { TypeORMError } from "../../error" +import { QueryResult } from "../../query-runner/QueryResult" +import { MetadataTableType } from "../types/MetadataTableType" +import { SpannerDriver } from "./SpannerDriver" + +/** + * Runs queries on a single postgres database connection. + */ +export class SpannerQueryRunner extends BaseQueryRunner implements QueryRunner { + // ------------------------------------------------------------------------- + // Public Implemented Properties + // ------------------------------------------------------------------------- + + /** + * Database driver used by connection. + */ + driver: SpannerDriver + + /** + * Real database connection from a connection pool used to perform queries. + */ + protected session?: any + + /** + * Transaction currently executed by this session. + */ + protected sessionTransaction?: any + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(driver: SpannerDriver, mode: ReplicationMode) { + super() + this.driver = driver + this.connection = driver.connection + this.mode = mode + this.broadcaster = new Broadcaster(this) + } + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + + /** + * Creates/uses database connection from the connection pool to perform further operations. + * Returns obtained database connection. + */ + async connect(): Promise { + if (this.session) { + return Promise.resolve(this.session) + } + + const [session] = await this.driver.instanceDatabase.createSession({}) + this.session = session + this.sessionTransaction = await session.transaction() + return this.session + } + + /** + * Releases used database connection. + * You cannot use query runner methods once its released. + */ + async release(): Promise { + this.isReleased = true + if (this.session) { + await this.session.delete() + } + this.session = undefined + return Promise.resolve() + } + + /** + * Starts transaction. + */ + async startTransaction(isolationLevel?: IsolationLevel): Promise { + this.isTransactionActive = true + try { + await this.broadcaster.broadcast("BeforeTransactionStart") + } catch (err) { + this.isTransactionActive = false + throw err + } + + await this.connect() + await this.sessionTransaction.begin() + this.connection.logger.logQuery("START TRANSACTION") + + await this.broadcaster.broadcast("AfterTransactionStart") + } + + /** + * Commits transaction. + * Error will be thrown if transaction was not started. + */ + async commitTransaction(): Promise { + if (!this.isTransactionActive || !this.sessionTransaction) + throw new TransactionNotStartedError() + + await this.broadcaster.broadcast("BeforeTransactionCommit") + + await this.sessionTransaction.commit() + this.connection.logger.logQuery("COMMIT") + this.isTransactionActive = false + + await this.broadcaster.broadcast("AfterTransactionCommit") + } + + /** + * Rollbacks transaction. + * Error will be thrown if transaction was not started. + */ + async rollbackTransaction(): Promise { + if (!this.isTransactionActive || !this.sessionTransaction) + throw new TransactionNotStartedError() + + await this.broadcaster.broadcast("BeforeTransactionRollback") + + await this.sessionTransaction.rollback() + this.connection.logger.logQuery("ROLLBACK") + this.isTransactionActive = false + + await this.broadcaster.broadcast("AfterTransactionRollback") + } + + /** + * Executes a given SQL query. + */ + async query( + query: string, + parameters?: any[], + useStructuredResult: boolean = false, + ): Promise { + if (this.isReleased) throw new QueryRunnerAlreadyReleasedError() + + try { + const queryStartTime = +new Date() + await this.connect() + let rawResult: + | [ + any[], + { + queryPlan: null + queryStats: null + rowCountExact: string + rowCount: string + }, + { rowType: { fields: [] }; transaction: null }, + ] + | undefined = undefined + const isSelect = query.startsWith("SELECT") + const executor = + isSelect && !this.isTransactionActive + ? this.driver.instanceDatabase + : this.sessionTransaction + + if (!this.isTransactionActive && !isSelect) { + await this.sessionTransaction.begin() + } + + try { + this.driver.connection.logger.logQuery(query, parameters, this) + rawResult = await executor.run({ + sql: query, + params: parameters + ? parameters.reduce((params, value, index) => { + params["param" + index] = value + return params + }, {} as ObjectLiteral) + : undefined, + json: true, + }) + if (!this.isTransactionActive && !isSelect) { + await this.sessionTransaction.commit() + } + } catch (error) { + try { + // we throw original error even if rollback thrown an error + if (!this.isTransactionActive && !isSelect) + await this.sessionTransaction.rollback() + } catch (rollbackError) {} + throw error + } + + // log slow queries if maxQueryExecution time is set + const maxQueryExecutionTime = + this.driver.options.maxQueryExecutionTime + const queryEndTime = +new Date() + const queryExecutionTime = queryEndTime - queryStartTime + if ( + maxQueryExecutionTime && + queryExecutionTime > maxQueryExecutionTime + ) + this.driver.connection.logger.logQuerySlow( + queryExecutionTime, + query, + parameters, + this, + ) + + const result = new QueryResult() + + result.raw = rawResult + result.records = rawResult ? rawResult[0] : [] + if (rawResult && rawResult[1] && rawResult[1].rowCountExact) { + result.affected = parseInt(rawResult[1].rowCountExact) + } + + if (!useStructuredResult) { + return result.records + } + + return result + } catch (err) { + this.driver.connection.logger.logQueryError( + err, + query, + parameters, + this, + ) + throw new QueryFailedError(query, parameters, err) + } finally { + } + } + + /** + * Update database schema. + * Used for creating/altering/dropping tables, columns, indexes, etc. + * + * DDL changing queries should be executed by `updateSchema()` method. + */ + async updateDDL(query: string, parameters?: any[]): Promise { + if (this.isReleased) throw new QueryRunnerAlreadyReleasedError() + + this.driver.connection.logger.logQuery(query, parameters, this) + try { + const queryStartTime = +new Date() + const [operation] = await this.driver.instanceDatabase.updateSchema( + query, + ) + await operation.promise() + // log slow queries if maxQueryExecution time is set + const maxQueryExecutionTime = + this.driver.options.maxQueryExecutionTime + const queryEndTime = +new Date() + const queryExecutionTime = queryEndTime - queryStartTime + if ( + maxQueryExecutionTime && + queryExecutionTime > maxQueryExecutionTime + ) + this.driver.connection.logger.logQuerySlow( + queryExecutionTime, + query, + parameters, + this, + ) + } catch (err) { + this.driver.connection.logger.logQueryError( + err, + query, + parameters, + this, + ) + throw new QueryFailedError(query, parameters, err) + } + } + + /** + * Returns raw data stream. + */ + async stream( + query: string, + parameters?: any[], + onEnd?: Function, + onError?: Function, + ): Promise { + if (this.isReleased) throw new QueryRunnerAlreadyReleasedError() + + try { + this.driver.connection.logger.logQuery(query, parameters, this) + const request = { + sql: query, + params: parameters + ? parameters.reduce((params, value, index) => { + params["param" + index] = value + return params + }, {} as ObjectLiteral) + : undefined, + json: true, + } + const stream = this.driver.instanceDatabase.runStream(request) + + if (onEnd) { + stream.on("end", onEnd) + } + + if (onError) { + stream.on("error", onError) + } + + return stream + } catch (err) { + this.driver.connection.logger.logQueryError( + err, + query, + parameters, + this, + ) + throw new QueryFailedError(query, parameters, err) + } + } + + /** + * Returns all available database names including system databases. + */ + async getDatabases(): Promise { + return Promise.resolve([]) + } + + /** + * Returns all available schema names including system schemas. + * If database parameter specified, returns schemas of that database. + */ + async getSchemas(database?: string): Promise { + return Promise.resolve([]) + } + + /** + * Checks if database with the given name exist. + */ + async hasDatabase(database: string): Promise { + throw new TypeORMError( + `Check database queries are not supported by Spanner driver.`, + ) + } + + /** + * Loads currently using database + */ + async getCurrentDatabase(): Promise { + throw new TypeORMError( + `Check database queries are not supported by Spanner driver.`, + ) + } + + /** + * Checks if schema with the given name exist. + */ + async hasSchema(schema: string): Promise { + const result = await this.query( + `SELECT * FROM "information_schema"."schemata" WHERE "schema_name" = '${schema}'`, + ) + return result.length ? true : false + } + + /** + * Loads currently using database schema + */ + async getCurrentSchema(): Promise { + throw new TypeORMError( + `Check schema queries are not supported by Spanner driver.`, + ) + } + + /** + * Checks if table with the given name exist in the database. + */ + async hasTable(tableOrName: Table | string): Promise { + const tableName = + tableOrName instanceof Table ? tableOrName.name : tableOrName + const sql = + `SELECT * FROM \`INFORMATION_SCHEMA\`.\`TABLES\` ` + + `WHERE \`TABLE_CATALOG\` = '' AND \`TABLE_SCHEMA\` = '' AND \`TABLE_TYPE\` = 'BASE TABLE' ` + + `AND \`TABLE_NAME\` = '${tableName}'` + const result = await this.query(sql) + return result.length ? true : false + } + + /** + * Checks if column with the given name exist in the given table. + */ + async hasColumn( + tableOrName: Table | string, + columnName: string, + ): Promise { + const tableName = + tableOrName instanceof Table ? tableOrName.name : tableOrName + const sql = + `SELECT * FROM \`INFORMATION_SCHEMA\`.\`COLUMNS\` ` + + `WHERE \`TABLE_CATALOG\` = '' AND \`TABLE_SCHEMA\` = '' ` + + `AND \`TABLE_NAME\` = '${tableName}' AND \`COLUMN_NAME\` = '${columnName}'` + const result = await this.query(sql) + return result.length ? true : false + } + + /** + * Creates a new database. + * Note: Spanner does not support database creation inside a transaction block. + */ + async createDatabase( + database: string, + ifNotExist?: boolean, + ): Promise { + if (ifNotExist) { + const databaseAlreadyExists = await this.hasDatabase(database) + + if (databaseAlreadyExists) return Promise.resolve() + } + + const up = `CREATE DATABASE "${database}"` + const down = `DROP DATABASE "${database}"` + await this.executeQueries(new Query(up), new Query(down)) + } + + /** + * Drops database. + * Note: Spanner does not support database dropping inside a transaction block. + */ + async dropDatabase(database: string, ifExist?: boolean): Promise { + const up = ifExist + ? `DROP DATABASE IF EXISTS "${database}"` + : `DROP DATABASE "${database}"` + const down = `CREATE DATABASE "${database}"` + await this.executeQueries(new Query(up), new Query(down)) + } + + /** + * Creates a new table schema. + */ + async createSchema( + schemaPath: string, + ifNotExist?: boolean, + ): Promise { + return Promise.resolve() + } + + /** + * Drops table schema. + */ + async dropSchema( + schemaPath: string, + ifExist?: boolean, + isCascade?: boolean, + ): Promise { + return Promise.resolve() + } + + /** + * Creates a new table. + */ + async createTable( + table: Table, + ifNotExist: boolean = false, + createForeignKeys: boolean = true, + createIndices: boolean = true, + ): Promise { + if (ifNotExist) { + const isTableExist = await this.hasTable(table) + if (isTableExist) return Promise.resolve() + } + const upQueries: Query[] = [] + const downQueries: Query[] = [] + + upQueries.push(this.createTableSql(table, createForeignKeys)) + downQueries.push(this.dropTableSql(table)) + + // if createForeignKeys is true, we must drop created foreign keys in down query. + // createTable does not need separate method to create foreign keys, because it create fk's in the same query with table creation. + if (createForeignKeys) + table.foreignKeys.forEach((foreignKey) => + downQueries.push(this.dropForeignKeySql(table, foreignKey)), + ) + + if (createIndices) { + table.indices.forEach((index) => { + // new index may be passed without name. In this case we generate index name manually. + if (!index.name) + index.name = this.connection.namingStrategy.indexName( + table, + index.columnNames, + index.where, + ) + upQueries.push(this.createIndexSql(table, index)) + downQueries.push(this.dropIndexSql(table, index)) + }) + } + + await this.executeQueries(upQueries, downQueries) + } + + /** + * Drops the table. + */ + async dropTable( + target: Table | string, + ifExist?: boolean, + dropForeignKeys: boolean = true, + dropIndices: boolean = true, + ): Promise { + // It needs because if table does not exist and dropForeignKeys or dropIndices is true, we don't need + // to perform drop queries for foreign keys and indices. + if (ifExist) { + const isTableExist = await this.hasTable(target) + if (!isTableExist) return Promise.resolve() + } + + // if dropTable called with dropForeignKeys = true, we must create foreign keys in down query. + const createForeignKeys: boolean = dropForeignKeys + const tablePath = this.getTablePath(target) + const table = await this.getCachedTable(tablePath) + const upQueries: Query[] = [] + const downQueries: Query[] = [] + + if (dropIndices) { + table.indices.forEach((index) => { + upQueries.push(this.dropIndexSql(table, index)) + downQueries.push(this.createIndexSql(table, index)) + }) + } + + if (dropForeignKeys) + table.foreignKeys.forEach((foreignKey) => + upQueries.push(this.dropForeignKeySql(table, foreignKey)), + ) + + upQueries.push(this.dropTableSql(table)) + downQueries.push(this.createTableSql(table, createForeignKeys)) + + await this.executeQueries(upQueries, downQueries) + } + + /** + * Creates a new view. + */ + async createView(view: View): Promise { + const upQueries: Query[] = [] + const downQueries: Query[] = [] + upQueries.push(this.createViewSql(view)) + upQueries.push(await this.insertViewDefinitionSql(view)) + downQueries.push(this.dropViewSql(view)) + downQueries.push(await this.deleteViewDefinitionSql(view)) + await this.executeQueries(upQueries, downQueries) + } + + /** + * Drops the view. + */ + async dropView(target: View | string): Promise { + const viewName = target instanceof View ? target.name : target + const view = await this.getCachedView(viewName) + + const upQueries: Query[] = [] + const downQueries: Query[] = [] + upQueries.push(await this.deleteViewDefinitionSql(view)) + upQueries.push(this.dropViewSql(view)) + downQueries.push(await this.insertViewDefinitionSql(view)) + downQueries.push(this.createViewSql(view)) + await this.executeQueries(upQueries, downQueries) + } + + /** + * Renames the given table. + */ + async renameTable( + oldTableOrName: Table | string, + newTableName: string, + ): Promise { + throw new TypeORMError( + `Rename table queries are not supported by Spanner driver.`, + ) + } + + /** + * Creates a new column from the column in the table. + */ + async addColumn( + tableOrName: Table | string, + column: TableColumn, + ): Promise { + const table = + tableOrName instanceof Table + ? tableOrName + : await this.getCachedTable(tableOrName) + const clonedTable = table.clone() + const upQueries: Query[] = [] + const downQueries: Query[] = [] + + upQueries.push( + new Query( + `ALTER TABLE ${this.escapePath( + table, + )} ADD ${this.buildCreateColumnSql(column)}`, + ), + ) + downQueries.push( + new Query( + `ALTER TABLE ${this.escapePath( + table, + )} DROP COLUMN ${this.driver.escape(column.name)}`, + ), + ) + + // create column index + const columnIndex = clonedTable.indices.find( + (index) => + index.columnNames.length === 1 && + index.columnNames[0] === column.name, + ) + if (columnIndex) { + upQueries.push(this.createIndexSql(table, columnIndex)) + downQueries.push(this.dropIndexSql(table, columnIndex)) + } else if (column.isUnique) { + const uniqueIndex = new TableIndex({ + name: this.connection.namingStrategy.indexName(table, [ + column.name, + ]), + columnNames: [column.name], + isUnique: true, + }) + clonedTable.indices.push(uniqueIndex) + clonedTable.uniques.push( + new TableUnique({ + name: uniqueIndex.name, + columnNames: uniqueIndex.columnNames, + }), + ) + + upQueries.push(this.createIndexSql(table, uniqueIndex)) + downQueries.push(this.dropIndexSql(table, uniqueIndex)) + } + + await this.executeQueries(upQueries, downQueries) + + clonedTable.addColumn(column) + this.replaceCachedTable(table, clonedTable) + } + + /** + * Creates a new columns from the column in the table. + */ + async addColumns( + tableOrName: Table | string, + columns: TableColumn[], + ): Promise { + for (const column of columns) { + await this.addColumn(tableOrName, column) + } + } + + /** + * Renames column in the given table. + */ + async renameColumn( + tableOrName: Table | string, + oldTableColumnOrName: TableColumn | string, + newTableColumnOrName: TableColumn | string, + ): Promise { + const table = + tableOrName instanceof Table + ? tableOrName + : await this.getCachedTable(tableOrName) + const oldColumn = + oldTableColumnOrName instanceof TableColumn + ? oldTableColumnOrName + : table.columns.find((c) => c.name === oldTableColumnOrName) + if (!oldColumn) + throw new TypeORMError( + `Column "${oldTableColumnOrName}" was not found in the "${table.name}" table.`, + ) + + let newColumn + if (newTableColumnOrName instanceof TableColumn) { + newColumn = newTableColumnOrName + } else { + newColumn = oldColumn.clone() + newColumn.name = newTableColumnOrName + } + + return this.changeColumn(table, oldColumn, newColumn) + } + + /** + * Changes a column in the table. + */ + async changeColumn( + tableOrName: Table | string, + oldTableColumnOrName: TableColumn | string, + newColumn: TableColumn, + ): Promise { + const table = + tableOrName instanceof Table + ? tableOrName + : await this.getCachedTable(tableOrName) + let clonedTable = table.clone() + const upQueries: Query[] = [] + const downQueries: Query[] = [] + + const oldColumn = + oldTableColumnOrName instanceof TableColumn + ? oldTableColumnOrName + : table.columns.find( + (column) => column.name === oldTableColumnOrName, + ) + if (!oldColumn) + throw new TypeORMError( + `Column "${oldTableColumnOrName}" was not found in the "${table.name}" table.`, + ) + + if ( + oldColumn.name !== newColumn.name || + oldColumn.type !== newColumn.type || + oldColumn.length !== newColumn.length || + newColumn.isArray !== oldColumn.isArray || + oldColumn.generatedType !== newColumn.generatedType + ) { + // To avoid data conversion, we just recreate column + await this.dropColumn(table, oldColumn) + await this.addColumn(table, newColumn) + + // update cloned table + clonedTable = table.clone() + } else { + if ( + newColumn.precision !== oldColumn.precision || + newColumn.scale !== oldColumn.scale + ) { + upQueries.push( + new Query( + `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${ + newColumn.name + }" TYPE ${this.driver.createFullType(newColumn)}`, + ), + ) + downQueries.push( + new Query( + `ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${ + newColumn.name + }" TYPE ${this.driver.createFullType(oldColumn)}`, + ), + ) + } + + if (oldColumn.isNullable !== newColumn.isNullable) { + if (newColumn.isNullable) { + upQueries.push( + new Query( + `ALTER TABLE ${this.escapePath( + table, + )} ALTER COLUMN "${oldColumn.name}" DROP NOT NULL`, + ), + ) + downQueries.push( + new Query( + `ALTER TABLE ${this.escapePath( + table, + )} ALTER COLUMN "${oldColumn.name}" SET NOT NULL`, + ), + ) + } else { + upQueries.push( + new Query( + `ALTER TABLE ${this.escapePath( + table, + )} ALTER COLUMN "${oldColumn.name}" SET NOT NULL`, + ), + ) + downQueries.push( + new Query( + `ALTER TABLE ${this.escapePath( + table, + )} ALTER COLUMN "${oldColumn.name}" DROP NOT NULL`, + ), + ) + } + } + + if (newColumn.isUnique !== oldColumn.isUnique) { + if (newColumn.isUnique === true) { + const uniqueIndex = new TableIndex({ + name: this.connection.namingStrategy.indexName(table, [ + newColumn.name, + ]), + columnNames: [newColumn.name], + isUnique: true, + }) + clonedTable.indices.push(uniqueIndex) + clonedTable.uniques.push( + new TableUnique({ + name: uniqueIndex.name, + columnNames: uniqueIndex.columnNames, + }), + ) + + upQueries.push(this.createIndexSql(table, uniqueIndex)) + downQueries.push(this.dropIndexSql(table, uniqueIndex)) + } else { + const uniqueIndex = clonedTable.indices.find((index) => { + return ( + index.columnNames.length === 1 && + index.isUnique === true && + !!index.columnNames.find( + (columnName) => columnName === newColumn.name, + ) + ) + }) + clonedTable.indices.splice( + clonedTable.indices.indexOf(uniqueIndex!), + 1, + ) + + const tableUnique = clonedTable.uniques.find( + (unique) => unique.name === uniqueIndex!.name, + ) + clonedTable.uniques.splice( + clonedTable.uniques.indexOf(tableUnique!), + 1, + ) + + upQueries.push(this.dropIndexSql(table, uniqueIndex!)) + downQueries.push(this.createIndexSql(table, uniqueIndex!)) + } + } + + if (newColumn.generatedType !== oldColumn.generatedType) { + } + } + + await this.executeQueries(upQueries, downQueries) + this.replaceCachedTable(table, clonedTable) + } + + /** + * Changes a column in the table. + */ + async changeColumns( + tableOrName: Table | string, + changedColumns: { newColumn: TableColumn; oldColumn: TableColumn }[], + ): Promise { + for (const { oldColumn, newColumn } of changedColumns) { + await this.changeColumn(tableOrName, oldColumn, newColumn) + } + } + + /** + * Drops column in the table. + */ + async dropColumn( + tableOrName: Table | string, + columnOrName: TableColumn | string, + ): Promise { + const table = + tableOrName instanceof Table + ? tableOrName + : await this.getCachedTable(tableOrName) + const column = + columnOrName instanceof TableColumn + ? columnOrName + : table.findColumnByName(columnOrName) + if (!column) + throw new TypeORMError( + `Column "${columnOrName}" was not found in table "${table.name}"`, + ) + + const clonedTable = table.clone() + const upQueries: Query[] = [] + const downQueries: Query[] = [] + + // drop column index + const columnIndex = clonedTable.indices.find( + (index) => + index.columnNames.length === 1 && + index.columnNames[0] === column.name, + ) + if (columnIndex) { + clonedTable.indices.splice( + clonedTable.indices.indexOf(columnIndex), + 1, + ) + upQueries.push(this.dropIndexSql(table, columnIndex)) + downQueries.push(this.createIndexSql(table, columnIndex)) + } + + // drop column check + const columnCheck = clonedTable.checks.find( + (check) => + !!check.columnNames && + check.columnNames.length === 1 && + check.columnNames[0] === column.name, + ) + if (columnCheck) { + clonedTable.checks.splice( + clonedTable.checks.indexOf(columnCheck), + 1, + ) + upQueries.push(this.dropCheckConstraintSql(table, columnCheck)) + downQueries.push(this.createCheckConstraintSql(table, columnCheck)) + } + + upQueries.push( + new Query( + `ALTER TABLE ${this.escapePath( + table, + )} DROP COLUMN ${this.driver.escape(column.name)}`, + ), + ) + downQueries.push( + new Query( + `ALTER TABLE ${this.escapePath( + table, + )} ADD ${this.buildCreateColumnSql(column)}`, + ), + ) + + await this.executeQueries(upQueries, downQueries) + + clonedTable.removeColumn(column) + this.replaceCachedTable(table, clonedTable) + } + + /** + * Drops the columns in the table. + */ + async dropColumns( + tableOrName: Table | string, + columns: TableColumn[] | string[], + ): Promise { + for (const column of columns) { + await this.dropColumn(tableOrName, column) + } + } + + /** + * Creates a new primary key. + * + * Not supported in Spanner. + * @see https://cloud.google.com/spanner/docs/schema-and-data-model#notes_about_key_columns + */ + async createPrimaryKey( + tableOrName: Table | string, + columnNames: string[], + ): Promise { + throw new Error( + "The keys of a table can't change; you can't add a key column to an existing table or remove a key column from an existing table.", + ) + } + + /** + * Updates composite primary keys. + */ + async updatePrimaryKeys( + tableOrName: Table | string, + columns: TableColumn[], + ): Promise { + throw new Error( + "The keys of a table can't change; you can't add a key column to an existing table or remove a key column from an existing table.", + ) + } + + /** + * Creates a new primary key. + * + * Not supported in Spanner. + * @see https://cloud.google.com/spanner/docs/schema-and-data-model#notes_about_key_columns + */ + async dropPrimaryKey(tableOrName: Table | string): Promise { + throw new Error( + "The keys of a table can't change; you can't add a key column to an existing table or remove a key column from an existing table.", + ) + } + + /** + * Creates new unique constraint. + */ + async createUniqueConstraint( + tableOrName: Table | string, + uniqueConstraint: TableUnique, + ): Promise { + throw new TypeORMError( + `Spanner does not support unique constraints. Use unique index instead.`, + ) + } + + /** + * Creates new unique constraints. + */ + async createUniqueConstraints( + tableOrName: Table | string, + uniqueConstraints: TableUnique[], + ): Promise { + throw new TypeORMError( + `Spanner does not support unique constraints. Use unique index instead.`, + ) + } + + /** + * Drops unique constraint. + */ + async dropUniqueConstraint( + tableOrName: Table | string, + uniqueOrName: TableUnique | string, + ): Promise { + throw new TypeORMError( + `Spanner does not support unique constraints. Use unique index instead.`, + ) + } + + /** + * Drops unique constraints. + */ + async dropUniqueConstraints( + tableOrName: Table | string, + uniqueConstraints: TableUnique[], + ): Promise { + throw new TypeORMError( + `Spanner does not support unique constraints. Use unique index instead.`, + ) + } + + /** + * Creates new check constraint. + */ + async createCheckConstraint( + tableOrName: Table | string, + checkConstraint: TableCheck, + ): Promise { + const table = + tableOrName instanceof Table + ? tableOrName + : await this.getCachedTable(tableOrName) + + // new check constraint may be passed without name. In this case we generate unique name manually. + if (!checkConstraint.name) + checkConstraint.name = + this.connection.namingStrategy.checkConstraintName( + table, + checkConstraint.expression!, + ) + + const up = this.createCheckConstraintSql(table, checkConstraint) + const down = this.dropCheckConstraintSql(table, checkConstraint) + await this.executeQueries(up, down) + table.addCheckConstraint(checkConstraint) + } + + /** + * Creates new check constraints. + */ + async createCheckConstraints( + tableOrName: Table | string, + checkConstraints: TableCheck[], + ): Promise { + const promises = checkConstraints.map((checkConstraint) => + this.createCheckConstraint(tableOrName, checkConstraint), + ) + await Promise.all(promises) + } + + /** + * Drops check constraint. + */ + async dropCheckConstraint( + tableOrName: Table | string, + checkOrName: TableCheck | string, + ): Promise { + const table = + tableOrName instanceof Table + ? tableOrName + : await this.getCachedTable(tableOrName) + const checkConstraint = + checkOrName instanceof TableCheck + ? checkOrName + : table.checks.find((c) => c.name === checkOrName) + if (!checkConstraint) + throw new TypeORMError( + `Supplied check constraint was not found in table ${table.name}`, + ) + + const up = this.dropCheckConstraintSql(table, checkConstraint) + const down = this.createCheckConstraintSql(table, checkConstraint) + await this.executeQueries(up, down) + table.removeCheckConstraint(checkConstraint) + } + + /** + * Drops check constraints. + */ + async dropCheckConstraints( + tableOrName: Table | string, + checkConstraints: TableCheck[], + ): Promise { + const promises = checkConstraints.map((checkConstraint) => + this.dropCheckConstraint(tableOrName, checkConstraint), + ) + await Promise.all(promises) + } + + /** + * Creates new exclusion constraint. + */ + async createExclusionConstraint( + tableOrName: Table | string, + exclusionConstraint: TableExclusion, + ): Promise { + throw new TypeORMError( + `Spanner does not support exclusion constraints.`, + ) + } + + /** + * Creates new exclusion constraints. + */ + async createExclusionConstraints( + tableOrName: Table | string, + exclusionConstraints: TableExclusion[], + ): Promise { + throw new TypeORMError( + `Spanner does not support exclusion constraints.`, + ) + } + + /** + * Drops exclusion constraint. + */ + async dropExclusionConstraint( + tableOrName: Table | string, + exclusionOrName: TableExclusion | string, + ): Promise { + throw new TypeORMError( + `Spanner does not support exclusion constraints.`, + ) + } + + /** + * Drops exclusion constraints. + */ + async dropExclusionConstraints( + tableOrName: Table | string, + exclusionConstraints: TableExclusion[], + ): Promise { + throw new TypeORMError( + `Spanner does not support exclusion constraints.`, + ) + } + + /** + * Creates a new foreign key. + */ + async createForeignKey( + tableOrName: Table | string, + foreignKey: TableForeignKey, + ): Promise { + const table = + tableOrName instanceof Table + ? tableOrName + : await this.getCachedTable(tableOrName) + + // new FK may be passed without name. In this case we generate FK name manually. + if (!foreignKey.name) + foreignKey.name = this.connection.namingStrategy.foreignKeyName( + table, + foreignKey.columnNames, + this.getTablePath(foreignKey), + foreignKey.referencedColumnNames, + ) + + const up = this.createForeignKeySql(table, foreignKey) + const down = this.dropForeignKeySql(table, foreignKey) + await this.executeQueries(up, down) + table.addForeignKey(foreignKey) + } + + /** + * Creates a new foreign keys. + */ + async createForeignKeys( + tableOrName: Table | string, + foreignKeys: TableForeignKey[], + ): Promise { + for (const foreignKey of foreignKeys) { + await this.createForeignKey(tableOrName, foreignKey) + } + } + + /** + * Drops a foreign key from the table. + */ + async dropForeignKey( + tableOrName: Table | string, + foreignKeyOrName: TableForeignKey | string, + ): Promise { + const table = + tableOrName instanceof Table + ? tableOrName + : await this.getCachedTable(tableOrName) + const foreignKey = + foreignKeyOrName instanceof TableForeignKey + ? foreignKeyOrName + : table.foreignKeys.find((fk) => fk.name === foreignKeyOrName) + if (!foreignKey) + throw new TypeORMError( + `Supplied foreign key was not found in table ${table.name}`, + ) + + const up = this.dropForeignKeySql(table, foreignKey) + const down = this.createForeignKeySql(table, foreignKey) + await this.executeQueries(up, down) + table.removeForeignKey(foreignKey) + } + + /** + * Drops a foreign keys from the table. + */ + async dropForeignKeys( + tableOrName: Table | string, + foreignKeys: TableForeignKey[], + ): Promise { + for (const foreignKey of foreignKeys) { + await this.dropForeignKey(tableOrName, foreignKey) + } + } + + /** + * Creates a new index. + */ + async createIndex( + tableOrName: Table | string, + index: TableIndex, + ): Promise { + const table = + tableOrName instanceof Table + ? tableOrName + : await this.getCachedTable(tableOrName) + + // new index may be passed without name. In this case we generate index name manually. + if (!index.name) + index.name = this.connection.namingStrategy.indexName( + table, + index.columnNames, + index.where, + ) + + const up = this.createIndexSql(table, index) + const down = this.dropIndexSql(table, index) + await this.executeQueries(up, down) + table.addIndex(index) + } + + /** + * Creates a new indices + */ + async createIndices( + tableOrName: Table | string, + indices: TableIndex[], + ): Promise { + for (const index of indices) { + await this.createIndex(tableOrName, index) + } + } + + /** + * Drops an index from the table. + */ + async dropIndex( + tableOrName: Table | string, + indexOrName: TableIndex | string, + ): Promise { + const table = + tableOrName instanceof Table + ? tableOrName + : await this.getCachedTable(tableOrName) + const index = + indexOrName instanceof TableIndex + ? indexOrName + : table.indices.find((i) => i.name === indexOrName) + if (!index) + throw new TypeORMError( + `Supplied index ${indexOrName} was not found in table ${table.name}`, + ) + + const up = this.dropIndexSql(table, index) + const down = this.createIndexSql(table, index) + await this.executeQueries(up, down) + table.removeIndex(index) + } + + /** + * Drops an indices from the table. + */ + async dropIndices( + tableOrName: Table | string, + indices: TableIndex[], + ): Promise { + for (const index of indices) { + await this.dropIndex(tableOrName, index) + } + } + + /** + * Clears all table contents. + * Spanner does not support TRUNCATE TABLE statement, so we use DELETE FROM. + */ + async clearTable(tableName: string): Promise { + await this.query(`DELETE FROM ${this.escapePath(tableName)} WHERE true`) + } + + /** + * Removes all tables from the currently connected database. + */ + async clearDatabase(): Promise { + // drop index queries + const selectIndexDropsQuery = + `SELECT concat('DROP INDEX \`', INDEX_NAME, '\`') AS \`query\` ` + + `FROM \`INFORMATION_SCHEMA\`.\`INDEXES\` ` + + `WHERE \`TABLE_CATALOG\` = '' AND \`TABLE_SCHEMA\` = '' AND \`INDEX_TYPE\` = 'INDEX' AND \`SPANNER_IS_MANAGED\` = false` + const dropIndexQueries: ObjectLiteral[] = await this.query( + selectIndexDropsQuery, + ) + + // drop foreign key queries + const selectFKDropsQuery = + `SELECT concat('ALTER TABLE \`', TABLE_NAME, '\`', ' DROP CONSTRAINT \`', CONSTRAINT_NAME, '\`') AS \`query\` ` + + `FROM \`INFORMATION_SCHEMA\`.\`TABLE_CONSTRAINTS\` ` + + `WHERE \`TABLE_CATALOG\` = '' AND \`TABLE_SCHEMA\` = '' AND \`CONSTRAINT_TYPE\` = 'FOREIGN KEY'` + const dropFKQueries: ObjectLiteral[] = await this.query( + selectFKDropsQuery, + ) + + // drop view queries + const selectViewDropsQuery = `SELECT concat('DROP VIEW \`', TABLE_NAME, '\`') AS \`query\` FROM \`INFORMATION_SCHEMA\`.\`VIEWS\`` + const dropViewQueries: ObjectLiteral[] = await this.query( + selectViewDropsQuery, + ) + + // drop table queries + const dropTablesQuery = + `SELECT concat('DROP TABLE \`', TABLE_NAME, '\`') AS \`query\` ` + + `FROM \`INFORMATION_SCHEMA\`.\`TABLES\` ` + + `WHERE \`TABLE_CATALOG\` = '' AND \`TABLE_SCHEMA\` = '' AND \`TABLE_TYPE\` = 'BASE TABLE'` + const dropTableQueries: ObjectLiteral[] = await this.query( + dropTablesQuery, + ) + + if ( + !dropIndexQueries.length && + !dropFKQueries.length && + !dropViewQueries.length && + !dropTableQueries.length + ) + return + + const isAnotherTransactionActive = this.isTransactionActive + if (!isAnotherTransactionActive) await this.startTransaction() + try { + for (let query of dropIndexQueries) { + await this.updateDDL(query["query"]) + } + for (let query of dropFKQueries) { + await this.updateDDL(query["query"]) + } + + for (let query of dropViewQueries) { + await this.updateDDL(query["query"]) + } + + for (let query of dropTableQueries) { + await this.updateDDL(query["query"]) + } + + await this.commitTransaction() + } catch (error) { + try { + // we throw original error even if rollback thrown an error + if (!isAnotherTransactionActive) + await this.rollbackTransaction() + } catch (rollbackError) {} + throw error + } + } + + // ------------------------------------------------------------------------- + // Override Methods + // ------------------------------------------------------------------------- + + /** + * Executes up sql queries. + */ + async executeMemoryUpSql(): Promise { + for (const { query, parameters } of this.sqlInMemory.upQueries) { + if (this.isDMLQuery(query)) { + await this.query(query, parameters) + } else { + await this.updateDDL(query, parameters) + } + } + } + + /** + * Executes down sql queries. + */ + async executeMemoryDownSql(): Promise { + for (const { + query, + parameters, + } of this.sqlInMemory.downQueries.reverse()) { + if (this.isDMLQuery(query)) { + await this.query(query, parameters) + } else { + await this.updateDDL(query, parameters) + } + } + } + + // ------------------------------------------------------------------------- + // Protected Methods + // ------------------------------------------------------------------------- + + protected async loadViews(viewNames?: string[]): Promise { + const hasTable = await this.hasTable(this.getTypeormMetadataTableName()) + if (!hasTable) { + return [] + } + + if (!viewNames) { + viewNames = [] + } + + const escapedViewNames = viewNames + .map((viewName) => `'${viewName}'`) + .join(", ") + + const query = + `SELECT \`T\`.*, \`V\`.\`VIEW_DEFINITION\` FROM ${this.escapePath( + this.getTypeormMetadataTableName(), + )} \`T\` ` + + `INNER JOIN \`INFORMATION_SCHEMA\`.\`VIEWS\` \`V\` ON \`V\`.\`TABLE_NAME\` = \`T\`.\`NAME\` ` + + `WHERE \`T\`.\`TYPE\` = '${MetadataTableType.VIEW}' ${ + viewNames.length + ? ` AND \`T\`.\`NAME\` IN (${escapedViewNames})` + : "" + }` + const dbViews = await this.query(query) + return dbViews.map((dbView: any) => { + const view = new View() + view.database = dbView["NAME"] + view.name = this.driver.buildTableName(dbView["NAME"]) + view.expression = dbView["NAME"] + return view + }) + } + + /** + * Loads all tables (with given names) from the database and creates a Table from them. + */ + protected async loadTables(tableNames?: string[]): Promise { + if (tableNames && tableNames.length === 0) { + return [] + } + + const dbTables: { TABLE_NAME: string }[] = [] + + if (!tableNames || !tableNames.length) { + // Since we don't have any of this data we have to do a scan + const tablesSql = + `SELECT \`TABLE_NAME\` ` + + `FROM \`INFORMATION_SCHEMA\`.\`TABLES\` ` + + `WHERE \`TABLE_CATALOG\` = '' AND \`TABLE_SCHEMA\` = '' AND \`TABLE_TYPE\` = 'BASE TABLE'` + dbTables.push(...(await this.query(tablesSql))) + } else { + const tablesSql = + `SELECT \`TABLE_NAME\` ` + + `FROM \`INFORMATION_SCHEMA\`.\`TABLES\` ` + + `WHERE \`TABLE_CATALOG\` = '' AND \`TABLE_SCHEMA\` = '' AND \`TABLE_TYPE\` = 'BASE TABLE' ` + + `AND \`TABLE_NAME\` IN (${tableNames + .map((tableName) => `'${tableName}'`) + .join(", ")})` + + dbTables.push(...(await this.query(tablesSql))) + } + + // if tables were not found in the db, no need to proceed + if (!dbTables.length) return [] + + const loadedTableNames = dbTables + .map((dbTable) => `'${dbTable.TABLE_NAME}'`) + .join(", ") + + const columnsSql = `SELECT * FROM \`INFORMATION_SCHEMA\`.\`COLUMNS\` WHERE \`TABLE_CATALOG\` = '' AND \`TABLE_SCHEMA\` = '' AND \`TABLE_NAME\` IN (${loadedTableNames})` + + const primaryKeySql = + `SELECT \`KCU\`.\`TABLE_NAME\`, \`KCU\`.\`COLUMN_NAME\` ` + + `FROM \`INFORMATION_SCHEMA\`.\`TABLE_CONSTRAINTS\` \`TC\` ` + + `INNER JOIN \`INFORMATION_SCHEMA\`.\`KEY_COLUMN_USAGE\` \`KCU\` ON \`KCU\`.\`CONSTRAINT_NAME\` = \`TC\`.\`CONSTRAINT_NAME\` ` + + `WHERE \`TC\`.\`TABLE_CATALOG\` = '' AND \`TC\`.\`TABLE_SCHEMA\` = '' AND \`TC\`.\`CONSTRAINT_TYPE\` = 'PRIMARY KEY' ` + + `AND \`TC\`.\`TABLE_NAME\` IN (${loadedTableNames})` + + const indicesSql = + `SELECT \`I\`.\`TABLE_NAME\`, \`I\`.\`INDEX_NAME\`, \`I\`.\`IS_UNIQUE\`, \`I\`.\`IS_NULL_FILTERED\`, \`IC\`.\`COLUMN_NAME\` ` + + `FROM \`INFORMATION_SCHEMA\`.\`INDEXES\` \`I\` ` + + `INNER JOIN \`INFORMATION_SCHEMA\`.\`INDEX_COLUMNS\` \`IC\` ON \`IC\`.\`INDEX_NAME\` = \`I\`.\`INDEX_NAME\` ` + + `AND \`IC\`.\`TABLE_NAME\` = \`I\`.\`TABLE_NAME\` ` + + `WHERE \`I\`.\`TABLE_CATALOG\` = '' AND \`I\`.\`TABLE_SCHEMA\` = '' AND \`I\`.\`TABLE_NAME\` IN (${loadedTableNames}) ` + + `AND \`I\`.\`INDEX_TYPE\` = 'INDEX' AND \`I\`.\`SPANNER_IS_MANAGED\` = false` + + const checksSql = + `SELECT \`TC\`.\`TABLE_NAME\`, \`TC\`.\`CONSTRAINT_NAME\`, \`CC\`.\`CHECK_CLAUSE\`, \`CCU\`.\`COLUMN_NAME\`` + + `FROM \`INFORMATION_SCHEMA\`.\`TABLE_CONSTRAINTS\` \`TC\` ` + + `INNER JOIN \`INFORMATION_SCHEMA\`.\`CONSTRAINT_COLUMN_USAGE\` \`CCU\` ON \`CCU\`.\`CONSTRAINT_NAME\` = \`TC\`.\`CONSTRAINT_NAME\` ` + + `INNER JOIN \`INFORMATION_SCHEMA\`.\`CHECK_CONSTRAINTS\` \`CC\` ON \`CC\`.\`CONSTRAINT_NAME\` = \`TC\`.\`CONSTRAINT_NAME\` ` + + `WHERE \`TC\`.\`TABLE_CATALOG\` = '' AND \`TC\`.\`TABLE_SCHEMA\` = '' AND \`TC\`.\`CONSTRAINT_TYPE\` = 'CHECK' ` + + `AND \`TC\`.\`TABLE_NAME\` IN (${loadedTableNames}) AND \`TC\`.\`CONSTRAINT_NAME\` NOT LIKE 'CK_IS_NOT_NULL%'` + + const foreignKeysSql = + `SELECT \`TC\`.\`TABLE_NAME\`, \`TC\`.\`CONSTRAINT_NAME\`, \`KCU\`.\`COLUMN_NAME\`, ` + + `\`CTU\`.\`TABLE_NAME\` AS \`REFERENCED_TABLE_NAME\`, \`CCU\`.\`COLUMN_NAME\` AS \`REFERENCED_COLUMN_NAME\`, ` + + `\`RC\`.\`UPDATE_RULE\`, \`RC\`.\`DELETE_RULE\` ` + + `FROM \`INFORMATION_SCHEMA\`.\`TABLE_CONSTRAINTS\` \`TC\` ` + + `INNER JOIN \`INFORMATION_SCHEMA\`.\`KEY_COLUMN_USAGE\` \`KCU\` ON \`KCU\`.\`CONSTRAINT_NAME\` = \`TC\`.\`CONSTRAINT_NAME\` ` + + `INNER JOIN \`INFORMATION_SCHEMA\`.\`CONSTRAINT_TABLE_USAGE\` \`CTU\` ON \`CTU\`.\`CONSTRAINT_NAME\` = \`TC\`.\`CONSTRAINT_NAME\` ` + + `INNER JOIN \`INFORMATION_SCHEMA\`.\`REFERENTIAL_CONSTRAINTS\` \`RC\` ON \`RC\`.\`CONSTRAINT_NAME\` = \`TC\`.\`CONSTRAINT_NAME\` ` + + `INNER JOIN \`INFORMATION_SCHEMA\`.\`CONSTRAINT_COLUMN_USAGE\` \`CCU\` ON \`CCU\`.\`CONSTRAINT_NAME\` = \`TC\`.\`CONSTRAINT_NAME\` ` + + `WHERE \`TC\`.\`TABLE_CATALOG\` = '' AND \`TC\`.\`TABLE_SCHEMA\` = '' AND \`TC\`.\`CONSTRAINT_TYPE\` = 'FOREIGN KEY' ` + + `AND \`TC\`.\`TABLE_NAME\` IN (${loadedTableNames})` + + const [ + dbColumns, + dbPrimaryKeys, + dbIndices, + dbChecks, + dbForeignKeys, + ]: ObjectLiteral[][] = await Promise.all([ + this.query(columnsSql), + this.query(primaryKeySql), + this.query(indicesSql), + this.query(checksSql), + this.query(foreignKeysSql), + ]) + + // create tables for loaded tables + return Promise.all( + dbTables.map(async (dbTable) => { + const table = new Table() + + table.name = this.driver.buildTableName(dbTable["TABLE_NAME"]) + + // create columns from the loaded columns + table.columns = dbColumns + .filter( + (dbColumn) => + dbColumn["TABLE_NAME"] === dbTable["TABLE_NAME"], + ) + .map((dbColumn) => { + const columnUniqueIndices = dbIndices.filter( + (dbIndex) => { + return ( + dbIndex["TABLE_NAME"] === + dbTable["TABLE_NAME"] && + dbIndex["COLUMN_NAME"] === + dbColumn["COLUMN_NAME"] && + dbIndex["IS_UNIQUE"] === true + ) + }, + ) + + const tableMetadata = + this.connection.entityMetadatas.find( + (metadata) => + this.getTablePath(table) === + this.getTablePath(metadata), + ) + const hasIgnoredIndex = + columnUniqueIndices.length > 0 && + tableMetadata && + tableMetadata.indices.some((index) => { + return columnUniqueIndices.some( + (uniqueIndex) => { + return ( + index.name === + uniqueIndex["INDEX_NAME"] && + index.synchronize === false + ) + }, + ) + }) + + const isConstraintComposite = columnUniqueIndices.every( + (uniqueIndex) => { + return dbIndices.some( + (dbIndex) => + dbIndex["INDEX_NAME"] === + uniqueIndex["INDEX_NAME"] && + dbIndex["COLUMN_NAME"] !== + dbColumn["COLUMN_NAME"], + ) + }, + ) + + const tableColumn = new TableColumn() + tableColumn.name = dbColumn["COLUMN_NAME"] + + let fullType = dbColumn["SPANNER_TYPE"].toLowerCase() + if (fullType.indexOf("array") !== -1) { + tableColumn.isArray = true + fullType = fullType.substring( + fullType.indexOf("<") + 1, + fullType.indexOf(">"), + ) + } + + if (fullType.indexOf("(") !== -1) { + tableColumn.type = fullType.substring( + 0, + fullType.indexOf("("), + ) + } else { + tableColumn.type = fullType + } + + if ( + this.driver.withLengthColumnTypes.indexOf( + tableColumn.type as ColumnType, + ) !== -1 + ) { + tableColumn.length = fullType.substring( + fullType.indexOf("(") + 1, + fullType.indexOf(")"), + ) + } + + if (dbColumn["IS_GENERATED"] === "ALWAYS") { + tableColumn.asExpression = + dbColumn["GENERATION_EXPRESSION"] + tableColumn.generatedType = + dbColumn["IS_STORED"] === "YES" + ? "STORED" + : "VIRTUAL" + } + + tableColumn.isUnique = + columnUniqueIndices.length > 0 && + !hasIgnoredIndex && + !isConstraintComposite + tableColumn.isNullable = + dbColumn["IS_NULLABLE"] === "YES" + tableColumn.isPrimary = dbPrimaryKeys.some( + (dbPrimaryKey) => { + return ( + dbPrimaryKey["TABLE_NAME"] === + dbColumn["TABLE_NAME"] && + dbPrimaryKey["COLUMN_NAME"] === + dbColumn["COLUMN_NAME"] + ) + }, + ) + + return tableColumn + }) + + const tableForeignKeys = dbForeignKeys.filter( + (dbForeignKey) => { + return ( + dbForeignKey["TABLE_NAME"] === dbTable["TABLE_NAME"] + ) + }, + ) + + table.foreignKeys = OrmUtils.uniq( + tableForeignKeys, + (dbForeignKey) => dbForeignKey["CONSTRAINT_NAME"], + ).map((dbForeignKey) => { + const foreignKeys = tableForeignKeys.filter( + (dbFk) => + dbFk["CONSTRAINT_NAME"] === + dbForeignKey["CONSTRAINT_NAME"], + ) + return new TableForeignKey({ + name: dbForeignKey["CONSTRAINT_NAME"], + columnNames: OrmUtils.uniq( + foreignKeys.map((dbFk) => dbFk["COLUMN_NAME"]), + ), + referencedDatabase: + dbForeignKey["REFERENCED_TABLE_SCHEMA"], + referencedTableName: + dbForeignKey["REFERENCED_TABLE_NAME"], + referencedColumnNames: OrmUtils.uniq( + foreignKeys.map( + (dbFk) => dbFk["REFERENCED_COLUMN_NAME"], + ), + ), + onDelete: dbForeignKey["DELETE_RULE"], + onUpdate: dbForeignKey["UPDATE_RULE"], + }) + }) + + const tableIndices = dbIndices.filter( + (dbIndex) => + dbIndex["TABLE_NAME"] === dbTable["TABLE_NAME"], + ) + + table.indices = OrmUtils.uniq( + tableIndices, + (dbIndex) => dbIndex["INDEX_NAME"], + ).map((constraint) => { + const indices = tableIndices.filter((index) => { + return index["INDEX_NAME"] === constraint["INDEX_NAME"] + }) + + return new TableIndex({ + table: table, + name: constraint["INDEX_NAME"], + columnNames: indices.map((i) => i["COLUMN_NAME"]), + isUnique: constraint["IS_UNIQUE"], + isNullFiltered: constraint["IS_NULL_FILTERED"], + }) + }) + + const tableChecks = dbChecks.filter( + (dbCheck) => + dbCheck["TABLE_NAME"] === dbTable["TABLE_NAME"], + ) + + table.checks = OrmUtils.uniq( + tableChecks, + (dbIndex) => dbIndex["CONSTRAINT_NAME"], + ).map((constraint) => { + const checks = tableChecks.filter( + (dbC) => + dbC["CONSTRAINT_NAME"] === + constraint["CONSTRAINT_NAME"], + ) + return new TableCheck({ + name: constraint["CONSTRAINT_NAME"], + columnNames: checks.map((c) => c["COLUMN_NAME"]), + expression: constraint["CHECK_CLAUSE"], + }) + }) + + return table + }), + ) + } + + /** + * Builds create table sql. + */ + protected createTableSql(table: Table, createForeignKeys?: boolean): Query { + const columnDefinitions = table.columns + .map((column) => this.buildCreateColumnSql(column)) + .join(", ") + let sql = `CREATE TABLE ${this.escapePath(table)} (${columnDefinitions}` + + // we create unique indexes instead of unique constraints, because Spanner does not have unique constraints. + // if we mark column as Unique, it means that we create UNIQUE INDEX. + table.columns + .filter((column) => column.isUnique) + .forEach((column) => { + const isUniqueIndexExist = table.indices.some((index) => { + return ( + index.columnNames.length === 1 && + !!index.isUnique && + index.columnNames.indexOf(column.name) !== -1 + ) + }) + const isUniqueConstraintExist = table.uniques.some((unique) => { + return ( + unique.columnNames.length === 1 && + unique.columnNames.indexOf(column.name) !== -1 + ) + }) + if (!isUniqueIndexExist && !isUniqueConstraintExist) + table.indices.push( + new TableIndex({ + name: this.connection.namingStrategy.uniqueConstraintName( + table, + [column.name], + ), + columnNames: [column.name], + isUnique: true, + }), + ) + }) + + // as Spanner does not have unique constraints, we must create table indices from table uniques and mark them as unique. + if (table.uniques.length > 0) { + table.uniques.forEach((unique) => { + const uniqueExist = table.indices.some( + (index) => index.name === unique.name, + ) + if (!uniqueExist) { + table.indices.push( + new TableIndex({ + name: unique.name, + columnNames: unique.columnNames, + isUnique: true, + }), + ) + } + }) + } + + if (table.checks.length > 0) { + const checksSql = table.checks + .map((check) => { + const checkName = check.name + ? check.name + : this.connection.namingStrategy.checkConstraintName( + table, + check.expression!, + ) + return `CONSTRAINT \`${checkName}\` CHECK (${check.expression})` + }) + .join(", ") + + sql += `, ${checksSql}` + } + + if (table.foreignKeys.length > 0 && createForeignKeys) { + const foreignKeysSql = table.foreignKeys + .map((fk) => { + const columnNames = fk.columnNames + .map((columnName) => `\`${columnName}\``) + .join(", ") + if (!fk.name) + fk.name = this.connection.namingStrategy.foreignKeyName( + table, + fk.columnNames, + this.getTablePath(fk), + fk.referencedColumnNames, + ) + const referencedColumnNames = fk.referencedColumnNames + .map((columnName) => `\`${columnName}\``) + .join(", ") + + return `CONSTRAINT \`${ + fk.name + }\` FOREIGN KEY (${columnNames}) REFERENCES ${this.escapePath( + this.getTablePath(fk), + )} (${referencedColumnNames})` + }) + .join(", ") + + sql += `, ${foreignKeysSql}` + } + + sql += `)` + + const primaryColumns = table.columns.filter( + (column) => column.isPrimary, + ) + if (primaryColumns.length > 0) { + const columnNames = primaryColumns + .map((column) => this.driver.escape(column.name)) + .join(", ") + sql += ` PRIMARY KEY (${columnNames})` + } + + return new Query(sql) + } + + /** + * Builds drop table sql. + */ + protected dropTableSql(tableOrPath: Table | string): Query { + return new Query(`DROP TABLE ${this.escapePath(tableOrPath)}`) + } + + protected createViewSql(view: View): Query { + const materializedClause = view.materialized ? "MATERIALIZED " : "" + const viewName = this.escapePath(view) + + const expression = + typeof view.expression === "string" + ? view.expression + : view.expression(this.connection).getQuery() + return new Query( + `CREATE ${materializedClause}VIEW ${viewName} SQL SECURITY INVOKER AS ${expression}`, + ) + } + + protected async insertViewDefinitionSql(view: View): Promise { + let { schema, tableName: name } = this.driver.parseTableName(view) + + const type = view.materialized + ? MetadataTableType.MATERIALIZED_VIEW + : MetadataTableType.VIEW + const expression = + typeof view.expression === "string" + ? view.expression.trim() + : view.expression(this.connection).getQuery() + return this.insertTypeormMetadataSql({ + type, + schema, + name, + value: expression, + }) + } + + /** + * Builds drop view sql. + */ + protected dropViewSql(view: View): Query { + const materializedClause = view.materialized ? "MATERIALIZED " : "" + return new Query( + `DROP ${materializedClause}VIEW ${this.escapePath(view)}`, + ) + } + + /** + * Builds remove view sql. + */ + protected async deleteViewDefinitionSql(view: View): Promise { + let { schema, tableName: name } = this.driver.parseTableName(view) + + const type = view.materialized + ? MetadataTableType.MATERIALIZED_VIEW + : MetadataTableType.VIEW + return this.deleteTypeormMetadataSql({ type, schema, name }) + } + + /** + * Builds create index sql. + */ + protected createIndexSql(table: Table, index: TableIndex): Query { + const columns = index.columnNames + .map((columnName) => this.driver.escape(columnName)) + .join(", ") + let indexType = "" + if (index.isUnique) indexType += "UNIQUE " + if (index.isNullFiltered) indexType += "NULL_FILTERED " + + return new Query( + `CREATE ${indexType}INDEX \`${index.name}\` ON ${this.escapePath( + table, + )} (${columns})`, + ) + } + + /** + * Builds drop index sql. + */ + protected dropIndexSql( + table: Table, + indexOrName: TableIndex | string, + ): Query { + let indexName = + indexOrName instanceof TableIndex ? indexOrName.name : indexOrName + return new Query(`DROP INDEX \`${indexName}\``) + } + + /** + * Builds create check constraint sql. + */ + protected createCheckConstraintSql( + table: Table, + checkConstraint: TableCheck, + ): Query { + return new Query( + `ALTER TABLE ${this.escapePath(table)} ADD CONSTRAINT \`${ + checkConstraint.name + }\` CHECK (${checkConstraint.expression})`, + ) + } + + /** + * Builds drop check constraint sql. + */ + protected dropCheckConstraintSql( + table: Table, + checkOrName: TableCheck | string, + ): Query { + const checkName = + checkOrName instanceof TableCheck ? checkOrName.name : checkOrName + return new Query( + `ALTER TABLE ${this.escapePath( + table, + )} DROP CONSTRAINT \`${checkName}\``, + ) + } + + /** + * Builds create foreign key sql. + */ + protected createForeignKeySql( + table: Table, + foreignKey: TableForeignKey, + ): Query { + const columnNames = foreignKey.columnNames + .map((column) => this.driver.escape(column)) + .join(", ") + const referencedColumnNames = foreignKey.referencedColumnNames + .map((column) => this.driver.escape(column)) + .join(",") + let sql = + `ALTER TABLE ${this.escapePath(table)} ADD CONSTRAINT \`${ + foreignKey.name + }\` FOREIGN KEY (${columnNames}) ` + + `REFERENCES ${this.escapePath( + this.getTablePath(foreignKey), + )} (${referencedColumnNames})` + + return new Query(sql) + } + + /** + * Builds drop foreign key sql. + */ + protected dropForeignKeySql( + table: Table, + foreignKeyOrName: TableForeignKey | string, + ): Query { + const foreignKeyName = + foreignKeyOrName instanceof TableForeignKey + ? foreignKeyOrName.name + : foreignKeyOrName + return new Query( + `ALTER TABLE ${this.escapePath( + table, + )} DROP CONSTRAINT \`${foreignKeyName}\``, + ) + } + + /** + * Escapes given table or view path. + */ + protected escapePath(target: Table | View | string): string { + const { tableName } = this.driver.parseTableName(target) + return `\`${tableName}\`` + } + + /** + * Builds a part of query to create/change a column. + */ + protected buildCreateColumnSql(column: TableColumn) { + let c = `${this.driver.escape( + column.name, + )} ${this.connection.driver.createFullType(column)}` + + if (column.asExpression) { + c += ` AS (${column.asExpression}) ${ + column.generatedType ? column.generatedType : "STORED" + }` + } else { + if (!column.isNullable) c += " NOT NULL" + } + + return c + } + + /** + * Executes sql used special for schema build. + */ + protected async executeQueries( + upQueries: Query | Query[], + downQueries: Query | Query[], + ): Promise { + if (upQueries instanceof Query) upQueries = [upQueries] + if (downQueries instanceof Query) downQueries = [downQueries] + + this.sqlInMemory.upQueries.push(...upQueries) + this.sqlInMemory.downQueries.push(...downQueries) + + // if sql-in-memory mode is enabled then simply store sql in memory and return + if (this.sqlMemoryMode === true) + return Promise.resolve() as Promise + + for (const { query, parameters } of upQueries) { + if (this.isDMLQuery(query)) { + await this.query(query, parameters) + } else { + await this.updateDDL(query, parameters) + } + } + } + + protected isDMLQuery(query: string): boolean { + return ( + query.startsWith("INSERT") || + query.startsWith("UPDATE") || + query.startsWith("DELETE") + ) + } +} diff --git a/src/driver/types/ColumnTypes.ts b/src/driver/types/ColumnTypes.ts index c207b5258b..105d4571bd 100644 --- a/src/driver/types/ColumnTypes.ts +++ b/src/driver/types/ColumnTypes.ts @@ -15,7 +15,7 @@ export type PrimaryGeneratedColumnType = | "decimal" // mysql, postgres, mssql, sqlite, sap | "smalldecimal" // sap | "fixed" // mysql - | "numeric" // postgres, mssql, sqlite + | "numeric" // postgres, mssql, sqlite, spanner | "number" // oracle /** @@ -47,7 +47,7 @@ export type WithPrecisionColumnType = | "time" // mysql, postgres, mssql, cockroachdb | "time with time zone" // postgres, cockroachdb | "time without time zone" // postgres - | "timestamp" // mysql, postgres, mssql, oracle, cockroachdb + | "timestamp" // mysql, postgres, mssql, oracle, cockroachdb, spanner | "timestamp without time zone" // postgres, cockroachdb | "timestamp with time zone" // postgres, oracle, cockroachdb | "timestamp with local time zone" // oracle @@ -74,7 +74,7 @@ export type WithLengthColumnType = | "raw" // oracle | "binary" // mssql | "varbinary" // mssql, sap - | "string" // cockroachdb + | "string" // cockroachdb, spanner export type WithWidthColumnType = | "tinyint" // mysql @@ -97,17 +97,18 @@ export type SimpleColumnType = | "integer" // postgres, oracle, sqlite, cockroachdb | "int4" // postgres, cockroachdb | "int8" // postgres, sqlite, cockroachdb - | "int64" // cockroachdb + | "int64" // cockroachdb, spanner | "unsigned big int" // sqlite | "float" // mysql, mssql, oracle, sqlite, sap | "float4" // postgres, cockroachdb | "float8" // postgres, cockroachdb + | "float64" // spanner | "smallmoney" // mssql | "money" // postgres, mssql // boolean types | "boolean" // postgres, sqlite, mysql, cockroachdb - | "bool" // postgres, mysql, cockroachdb + | "bool" // postgres, mysql, cockroachdb, spanner // text/binary types | "tinyblob" // mysql @@ -123,7 +124,7 @@ export type SimpleColumnType = | "longtext" // mysql | "alphanum" // sap | "shorttext" // sap - | "bytes" // cockroachdb + | "bytes" // cockroachdb, spanner | "bytea" // postgres, cockroachdb | "long" // oracle | "raw" // oracle @@ -138,7 +139,7 @@ export type SimpleColumnType = | "timestamptz" // postgres, cockroachdb | "timestamp with local time zone" // oracle | "smalldatetime" // mssql - | "date" // mysql, postgres, mssql, oracle, sqlite + | "date" // mysql, postgres, mssql, oracle, sqlite, spanner | "interval year to month" // oracle | "interval day to second" // oracle | "interval" // postgres, cockroachdb @@ -184,7 +185,7 @@ export type SimpleColumnType = | "tsquery" // postgres | "uuid" // postgres, cockroachdb | "xml" // mssql, postgres - | "json" // mysql, postgres, cockroachdb + | "json" // mysql, postgres, cockroachdb, spanner | "jsonb" // postgres, cockroachdb | "varbinary" // mssql, sap | "hierarchyid" // mssql @@ -193,7 +194,7 @@ export type SimpleColumnType = | "urowid" // oracle | "uniqueidentifier" // mssql | "rowversion" // mssql - | "array" // cockroachdb, sap + | "array" // cockroachdb, sap, spanner | "cube" // postgres | "ltree" // postgres diff --git a/src/driver/types/DatabaseType.ts b/src/driver/types/DatabaseType.ts index 501fd39b98..ec686f64fa 100644 --- a/src/driver/types/DatabaseType.ts +++ b/src/driver/types/DatabaseType.ts @@ -20,3 +20,4 @@ export type DatabaseType = | "expo" | "better-sqlite3" | "capacitor" + | "spanner" diff --git a/src/entity-schema/EntitySchemaIndexOptions.ts b/src/entity-schema/EntitySchemaIndexOptions.ts index ec44b5ff74..0b448d929f 100644 --- a/src/entity-schema/EntitySchemaIndexOptions.ts +++ b/src/entity-schema/EntitySchemaIndexOptions.ts @@ -38,6 +38,15 @@ export interface EntitySchemaIndexOptions { */ fulltext?: boolean + /** + * NULL_FILTERED indexes are particularly useful for indexing sparse columns, where most rows contain a NULL value. + * In these cases, the NULL_FILTERED index can be considerably smaller and more efficient to maintain than + * a normal index that includes NULL values. + * + * Works only in Spanner. + */ + nullFiltered?: boolean + /** * Fulltext parser. * Works only in MySQL. diff --git a/src/entity-schema/EntitySchemaTransformer.ts b/src/entity-schema/EntitySchemaTransformer.ts index bb886cc087..09e1cde5ea 100644 --- a/src/entity-schema/EntitySchemaTransformer.ts +++ b/src/entity-schema/EntitySchemaTransformer.ts @@ -240,6 +240,7 @@ export class EntitySchemaTransformer { unique: index.unique === true ? true : false, spatial: index.spatial === true ? true : false, fulltext: index.fulltext === true ? true : false, + nullFiltered: index.nullFiltered === true ? true : false, parser: index.parser, synchronize: index.synchronize === false ? false : true, where: index.where, diff --git a/src/logger/AdvancedConsoleLogger.ts b/src/logger/AdvancedConsoleLogger.ts index 21ada4561b..8d2e31de71 100644 --- a/src/logger/AdvancedConsoleLogger.ts +++ b/src/logger/AdvancedConsoleLogger.ts @@ -146,7 +146,7 @@ export class AdvancedConsoleLogger implements Logger { /** * Converts parameters to a string. - * Sometimes parameters can have circular objects and therefor we are handle this case too. + * Sometimes parameters can have circular objects and therefore we are handle this case too. */ protected stringifyParams(parameters: any[]) { try { diff --git a/src/metadata-args/IndexMetadataArgs.ts b/src/metadata-args/IndexMetadataArgs.ts index 9f4f3160ed..74a877f4dc 100644 --- a/src/metadata-args/IndexMetadataArgs.ts +++ b/src/metadata-args/IndexMetadataArgs.ts @@ -34,6 +34,15 @@ export interface IndexMetadataArgs { */ fulltext?: boolean + /** + * NULL_FILTERED indexes are particularly useful for indexing sparse columns, where most rows contain a NULL value. + * In these cases, the NULL_FILTERED index can be considerably smaller and more efficient to maintain than + * a normal index that includes NULL values. + * + * Works only in Spanner. + */ + nullFiltered?: boolean + /** * Fulltext parser. * Works only in MySQL. diff --git a/src/metadata-builder/EntityMetadataBuilder.ts b/src/metadata-builder/EntityMetadataBuilder.ts index 3db6eb5e1c..24fd9a75a1 100644 --- a/src/metadata-builder/EntityMetadataBuilder.ts +++ b/src/metadata-builder/EntityMetadataBuilder.ts @@ -197,7 +197,9 @@ export class EntityMetadataBuilder { "aurora-mysql" || this.connection.driver.options.type === "mssql" || - this.connection.driver.options.type === "sap" + this.connection.driver.options.type === "sap" || + this.connection.driver.options.type === + "spanner" ) { const index = new IndexMetadata({ entityMetadata: @@ -224,6 +226,13 @@ export class EntityMetadataBuilder { .join(" AND ") } + if ( + this.connection.driver.options.type === + "spanner" + ) { + index.isNullFiltered = true + } + if (relation.embeddedMetadata) { relation.embeddedMetadata.indices.push( index, @@ -615,7 +624,7 @@ export class EntityMetadataBuilder { propertyName: "mpath", options: /*tree.column || */ { name: namingStrategy.materializedPathColumnName, - type: "varchar", + type: String, nullable: true, default: "", }, @@ -635,7 +644,7 @@ export class EntityMetadataBuilder { propertyName: left, options: /*tree.column || */ { name: left, - type: "integer", + type: Number, nullable: false, default: 1, }, @@ -653,7 +662,7 @@ export class EntityMetadataBuilder { propertyName: right, options: /*tree.column || */ { name: right, - type: "integer", + type: Number, nullable: false, default: 2, }, @@ -764,11 +773,12 @@ export class EntityMetadataBuilder { }) } - // Mysql and SAP HANA stores unique constraints as unique indices. + // This drivers stores unique constraints as unique indices. if ( DriverUtils.isMySQLFamily(this.connection.driver) || this.connection.driver.options.type === "aurora-mysql" || - this.connection.driver.options.type === "sap" + this.connection.driver.options.type === "sap" || + this.connection.driver.options.type === "spanner" ) { const indices = this.metadataArgsStorage .filterUniques(entityMetadata.inheritanceTree) diff --git a/src/metadata-builder/JunctionEntityMetadataBuilder.ts b/src/metadata-builder/JunctionEntityMetadataBuilder.ts index 8e9584ca47..06ff0f753b 100644 --- a/src/metadata-builder/JunctionEntityMetadataBuilder.ts +++ b/src/metadata-builder/JunctionEntityMetadataBuilder.ts @@ -207,6 +207,7 @@ export class JunctionEntityMetadataBuilder { // create junction table foreign keys // Note: UPDATE CASCADE clause is not supported in Oracle. + // Note: UPDATE/DELETE CASCADE clauses are not supported in Spanner. entityMetadata.foreignKeys = relation.createForeignKeyConstraints ? [ new ForeignKeyMetadata({ @@ -214,9 +215,13 @@ export class JunctionEntityMetadataBuilder { referencedEntityMetadata: relation.entityMetadata, columns: junctionColumns, referencedColumns: referencedColumns, - onDelete: relation.onDelete || "CASCADE", + onDelete: + this.connection.driver.options.type === "spanner" + ? "NO ACTION" + : relation.onDelete || "CASCADE", onUpdate: - this.connection.driver.options.type === "oracle" + this.connection.driver.options.type === "oracle" || + this.connection.driver.options.type === "spanner" ? "NO ACTION" : relation.onUpdate || "CASCADE", }), @@ -225,11 +230,15 @@ export class JunctionEntityMetadataBuilder { referencedEntityMetadata: relation.inverseEntityMetadata, columns: inverseJunctionColumns, referencedColumns: inverseReferencedColumns, - onDelete: relation.inverseRelation - ? relation.inverseRelation.onDelete - : "CASCADE", + onDelete: + this.connection.driver.options.type === "spanner" + ? "NO ACTION" + : relation.inverseRelation + ? relation.inverseRelation.onDelete + : "CASCADE", onUpdate: - this.connection.driver.options.type === "oracle" + this.connection.driver.options.type === "oracle" || + this.connection.driver.options.type === "spanner" ? "NO ACTION" : relation.inverseRelation ? relation.inverseRelation.onUpdate diff --git a/src/metadata/IndexMetadata.ts b/src/metadata/IndexMetadata.ts index 75a169aff9..5db4705bb7 100644 --- a/src/metadata/IndexMetadata.ts +++ b/src/metadata/IndexMetadata.ts @@ -40,6 +40,15 @@ export class IndexMetadata { */ isFulltext: boolean = false + /** + * NULL_FILTERED indexes are particularly useful for indexing sparse columns, where most rows contain a NULL value. + * In these cases, the NULL_FILTERED index can be considerably smaller and more efficient to maintain than + * a normal index that includes NULL values. + * + * Works only in Spanner. + */ + isNullFiltered: boolean = false + /** * Fulltext parser. * Works only in MySQL. @@ -134,6 +143,7 @@ export class IndexMetadata { this.isUnique = !!options.args.unique this.isSpatial = !!options.args.spatial this.isFulltext = !!options.args.fulltext + this.isNullFiltered = !!options.args.nullFiltered this.parser = options.args.parser this.where = options.args.where this.isSparse = options.args.sparse diff --git a/src/platform/PlatformTools.ts b/src/platform/PlatformTools.ts index c3f2b4b42c..d2a5d8aab1 100644 --- a/src/platform/PlatformTools.ts +++ b/src/platform/PlatformTools.ts @@ -35,8 +35,13 @@ export class PlatformTools { try { // switch case to explicit require statements for webpack compatibility. - switch (name) { + /** + * spanner + */ + case "spanner": + return require("@google-cloud/spanner") + /** * mongodb */ diff --git a/src/query-builder/InsertQueryBuilder.ts b/src/query-builder/InsertQueryBuilder.ts index 6c2d336243..bb585df7ed 100644 --- a/src/query-builder/InsertQueryBuilder.ts +++ b/src/query-builder/InsertQueryBuilder.ts @@ -626,6 +626,7 @@ export class InsertQueryBuilder extends QueryBuilder { if ( column.isGenerated && column.generationStrategy === "increment" && + !(this.connection.driver.options.type === "spanner") && !(this.connection.driver.options.type === "oracle") && !DriverUtils.isSQLiteFamily(this.connection.driver) && !DriverUtils.isMySQLFamily(this.connection.driver) && @@ -778,7 +779,8 @@ export class InsertQueryBuilder extends QueryBuilder { DriverUtils.isSQLiteFamily( this.connection.driver, ) || - this.connection.driver.options.type === "sap" + this.connection.driver.options.type === "sap" || + this.connection.driver.options.type === "spanner" ) { // unfortunately sqlite does not support DEFAULT expression in INSERT queries if ( @@ -796,6 +798,11 @@ export class InsertQueryBuilder extends QueryBuilder { } else { expression += "DEFAULT" } + } else if ( + value === null && + this.connection.driver.options.type === "spanner" + ) { + expression += "NULL" // support for SQL expressions in queries } else if (typeof value === "function") { @@ -929,16 +936,22 @@ export class InsertQueryBuilder extends QueryBuilder { // if value for this column was not provided then insert default value } else if (value === undefined) { if ( + (this.connection.driver.options.type === "oracle" && + valueSets.length > 1) || DriverUtils.isSQLiteFamily( this.connection.driver, ) || - this.connection.driver.options.type === "sap" + this.connection.driver.options.type === "sap" || + this.connection.driver.options.type === "spanner" ) { expression += "NULL" } else { expression += "DEFAULT" } - + } else if ( + value === null && + this.connection.driver.options.type === "spanner" + ) { // just any other regular value } else { expression += this.createParameter(value) diff --git a/src/query-builder/SelectQueryBuilder.ts b/src/query-builder/SelectQueryBuilder.ts index a68385787b..b396c5d5f2 100644 --- a/src/query-builder/SelectQueryBuilder.ts +++ b/src/query-builder/SelectQueryBuilder.ts @@ -2427,7 +2427,8 @@ export class SelectQueryBuilder } else if ( DriverUtils.isMySQLFamily(this.connection.driver) || this.connection.driver.options.type === "aurora-mysql" || - this.connection.driver.options.type === "sap" + this.connection.driver.options.type === "sap" || + this.connection.driver.options.type === "spanner" ) { if (limit && offset) return " LIMIT " + limit + " OFFSET " + offset if (limit) return " LIMIT " + limit @@ -2805,6 +2806,27 @@ export class SelectQueryBuilder return `COUNT(DISTINCT(CONCAT(${columnsExpression})))` } + if (this.connection.driver.options.type === "spanner") { + // spanner also has gotta be different from everyone else. + // they do not support concatenation of different column types without casting them to string + + if (primaryColumns.length === 1) { + return `COUNT(DISTINCT(${distinctAlias}.${this.escape( + primaryColumns[0].databaseName, + )}))` + } + + const columnsExpression = primaryColumns + .map( + (primaryColumn) => + `CAST(${distinctAlias}.${this.escape( + primaryColumn.databaseName, + )} AS STRING)`, + ) + .join(", '|;|', ") + return `COUNT(DISTINCT(CONCAT(${columnsExpression})))` + } + // If all else fails, fall back to a `COUNT` and `DISTINCT` across all the primary columns concatenated. // Per the SQL spec, this is the canonical string concatenation mechanism which is most // likely to work across servers implementing the SQL standard. @@ -3218,7 +3240,9 @@ export class SelectQueryBuilder primaryColumn.databaseName, ) - return `${distinctAlias}.${columnAlias} as "${alias}"` + return `${distinctAlias}.${columnAlias} AS ${this.escape( + alias, + )}` }, ) diff --git a/src/query-builder/UpdateQueryBuilder.ts b/src/query-builder/UpdateQueryBuilder.ts index 06973b407b..37185976c9 100644 --- a/src/query-builder/UpdateQueryBuilder.ts +++ b/src/query-builder/UpdateQueryBuilder.ts @@ -478,11 +478,19 @@ export class UpdateQueryBuilder ? this.expressionMap.mainAlias!.metadata : undefined + // it doesn't make sense to update undefined properties, so just skip them + const valuesSetNormalized: ObjectLiteral = {} + for (let key in valuesSet) { + if (valuesSet[key] !== undefined) { + valuesSetNormalized[key] = valuesSet[key] + } + } + // prepare columns and values to be updated const updateColumnAndValues: string[] = [] const updatedColumns: ColumnMetadata[] = [] if (metadata) { - this.createPropertyPath(metadata, valuesSet).forEach( + this.createPropertyPath(metadata, valuesSetNormalized).forEach( (propertyPath) => { // todo: make this and other query builder to work with properly with tables without metadata const columns = @@ -506,10 +514,11 @@ export class UpdateQueryBuilder updatedColumns.push(column) // - let value = column.getEntityValue(valuesSet) + let value = column.getEntityValue(valuesSetNormalized) if ( column.referencedColumn && typeof value === "object" && + value !== null && !Buffer.isBuffer(value) ) { value = @@ -531,7 +540,9 @@ export class UpdateQueryBuilder value(), ) } else if ( - this.connection.driver.options.type === "sap" && + (this.connection.driver.options.type === "sap" || + this.connection.driver.options.type === + "spanner") && value === null ) { updateColumnAndValues.push( @@ -614,7 +625,7 @@ export class UpdateQueryBuilder // Don't allow calling update only with columns that are `update: false` if ( updateColumnAndValues.length > 0 || - Object.keys(valuesSet).length === 0 + Object.keys(valuesSetNormalized).length === 0 ) { if ( metadata.versionColumn && @@ -636,8 +647,8 @@ export class UpdateQueryBuilder ) // todo: fix issue with CURRENT_TIMESTAMP(6) being used, can "DEFAULT" be used?! } } else { - Object.keys(valuesSet).map((key) => { - let value = valuesSet[key] + Object.keys(valuesSetNormalized).map((key) => { + let value = valuesSetNormalized[key] // todo: duplication zone if (typeof value === "function") { @@ -646,7 +657,8 @@ export class UpdateQueryBuilder this.escape(key) + " = " + value(), ) } else if ( - this.connection.driver.options.type === "sap" && + (this.connection.driver.options.type === "sap" || + this.connection.driver.options.type === "spanner") && value === null ) { updateColumnAndValues.push(this.escape(key) + " = NULL") diff --git a/src/schema-builder/RdbmsSchemaBuilder.ts b/src/schema-builder/RdbmsSchemaBuilder.ts index 077e35d43b..aa922aa1c2 100644 --- a/src/schema-builder/RdbmsSchemaBuilder.ts +++ b/src/schema-builder/RdbmsSchemaBuilder.ts @@ -66,8 +66,11 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { // CockroachDB implements asynchronous schema sync operations which can not been executed in transaction. // E.g. if you try to DROP column and ADD it again in the same transaction, crdb throws error. + // In Spanner queries against the INFORMATION_SCHEMA can be used in a read-only transaction, + // but not in a read-write transaction. const isUsingTransactions = !(this.connection.driver.options.type === "cockroachdb") && + !(this.connection.driver.options.type === "spanner") && this.connection.options.migrationsTransactionMode !== "none" await this.queryRunner.beforeMigration() @@ -813,7 +816,8 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { if ( !( DriverUtils.isMySQLFamily(this.connection.driver) || - this.connection.driver.options.type === "aurora-mysql" + this.connection.driver.options.type === "aurora-mysql" || + this.connection.driver.options.type === "spanner" ) ) { for (const changedColumn of changedColumns) { @@ -1181,6 +1185,10 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { database, ) + // Spanner requires at least one primary key in a table. + // Since we don't have unique column in "typeorm_metadata" table + // and we should avoid breaking changes, we mark all columns as primary for Spanner driver. + const isPrimary = this.connection.driver.options.type === "spanner" await queryRunner.createTable( new Table({ database: database, @@ -1194,6 +1202,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { .metadataType, }), isNullable: false, + isPrimary, }, { name: "database", @@ -1202,6 +1211,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { .metadataDatabase, }), isNullable: true, + isPrimary, }, { name: "schema", @@ -1210,6 +1220,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { .metadataSchema, }), isNullable: true, + isPrimary, }, { name: "table", @@ -1218,6 +1229,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { .metadataTable, }), isNullable: true, + isPrimary, }, { name: "name", @@ -1226,6 +1238,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { .metadataName, }), isNullable: true, + isPrimary, }, { name: "value", @@ -1234,6 +1247,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { .metadataValue, }), isNullable: true, + isPrimary, }, ], }), diff --git a/src/schema-builder/options/TableIndexOptions.ts b/src/schema-builder/options/TableIndexOptions.ts index 9747ddea9b..7999055042 100644 --- a/src/schema-builder/options/TableIndexOptions.ts +++ b/src/schema-builder/options/TableIndexOptions.ts @@ -33,6 +33,15 @@ export interface TableIndexOptions { */ isFulltext?: boolean + /** + * NULL_FILTERED indexes are particularly useful for indexing sparse columns, where most rows contain a NULL value. + * In these cases, the NULL_FILTERED index can be considerably smaller and more efficient to maintain than + * a normal index that includes NULL values. + * + * Works only in Spanner. + */ + isNullFiltered?: boolean + /** * Fulltext parser. * Works only in MySQL. diff --git a/src/schema-builder/table/TableIndex.ts b/src/schema-builder/table/TableIndex.ts index 39d7138d06..7662f427d2 100644 --- a/src/schema-builder/table/TableIndex.ts +++ b/src/schema-builder/table/TableIndex.ts @@ -38,6 +38,15 @@ export class TableIndex { */ isFulltext: boolean + /** + * NULL_FILTERED indexes are particularly useful for indexing sparse columns, where most rows contain a NULL value. + * In these cases, the NULL_FILTERED index can be considerably smaller and more efficient to maintain than + * a normal index that includes NULL values. + * + * Works only in Spanner. + */ + isNullFiltered: boolean + /** * Fulltext parser. * Works only in MySQL. @@ -59,6 +68,7 @@ export class TableIndex { this.isUnique = !!options.isUnique this.isSpatial = !!options.isSpatial this.isFulltext = !!options.isFulltext + this.isNullFiltered = !!options.isNullFiltered this.parser = options.parser this.where = options.where ? options.where : "" } @@ -77,6 +87,7 @@ export class TableIndex { isUnique: this.isUnique, isSpatial: this.isSpatial, isFulltext: this.isFulltext, + isNullFiltered: this.isNullFiltered, parser: this.parser, where: this.where, }) @@ -98,6 +109,7 @@ export class TableIndex { isUnique: indexMetadata.isUnique, isSpatial: indexMetadata.isSpatial, isFulltext: indexMetadata.isFulltext, + isNullFiltered: indexMetadata.isNullFiltered, parser: indexMetadata.parser, where: indexMetadata.where, }) diff --git a/test/__spanner-test/Dockerfile b/test/__spanner-test/Dockerfile new file mode 100644 index 0000000000..8f85897476 --- /dev/null +++ b/test/__spanner-test/Dockerfile @@ -0,0 +1,9 @@ +FROM google/cloud-sdk:slim + +RUN apt-get install -y google-cloud-sdk google-cloud-sdk-spanner-emulator + +COPY ./start_spanner.bash start_spanner.bash + +RUN ["chmod", "+x", "./start_spanner.bash"] + +CMD ./start_spanner.bash diff --git a/test/__spanner-test/spanner-test.ts b/test/__spanner-test/spanner-test.ts new file mode 100644 index 0000000000..5cf5157be6 --- /dev/null +++ b/test/__spanner-test/spanner-test.ts @@ -0,0 +1,81 @@ +// import { Spanner } from "@google-cloud/spanner" +// +// process.env.SPANNER_EMULATOR_HOST = "localhost:9010" +// // process.env.GOOGLE_APPLICATION_CREDENTIALS="/Users/messer/Documents/google/astute-cumulus-342713-80000a3b5bdb.json" +// +// async function main() { +// const projectId = "test-project" +// const instanceId = "test-instance" +// const databaseId = "test-db" +// +// const spanner = new Spanner({ +// projectId: projectId, +// }) +// +// const instance = spanner.instance(instanceId) +// const database = instance.database(databaseId) +// +// // const [operation] = await database.updateSchema( +// // `CREATE TABLE \`test\` (\`id\` INT64, \`name\` STRING(MAX)) PRIMARY KEY (\`id\`)`, +// // ) +// // await operation.promise() +// // const [tx] = await database.getTransaction() +// // await tx.runUpdate(`INSERT INTO \`book\`(\`ean\`) VALUES ('asd')`) +// // await tx.commit() +// +// // await database.run(`INSERT INTO \`book\`(\`ean\`) VALUES ('asd')`) +// +// const [session] = await database.createSession({}) +// const sessionTransaction = await session.transaction() +// +// // await sessionTransaction.begin() +// // await sessionTransaction.run({ +// // sql: `INSERT INTO \`test\`(\`id\`, \`name\`) VALUES (@param0, @param1)`, +// // params: { +// // param0: 2, +// // param1: null, +// // }, +// // types: { +// // param0: "int64", +// // param1: "string", +// // }, +// // }) +// // await sessionTransaction.commit() +// +// await sessionTransaction.begin() +// const [rows] = await sessionTransaction.run({ +// sql: `SELECT * FROM test WHERE name = @name AND id = @id`, +// params: { +// id: Spanner.int(2), +// name: null, +// }, +// types: { +// id: "int64", +// name: "string", +// }, +// json: true, +// }) +// await sessionTransaction.commit() +// console.log(rows) +// +// // const first = async () => { +// // const sessionTransaction = await session.transaction() +// // await sessionTransaction.begin() +// // await sessionTransaction.run(`INSERT INTO \`category\`(\`id\`, \`name\`) VALUES (1, 'aaa')`) +// // await sessionTransaction.commit() +// // } +// // +// // const second = async () => { +// // const sessionTransaction = await session.transaction() +// // await sessionTransaction.begin() +// // await sessionTransaction.run(`INSERT INTO \`category\`(\`id\`, \`name\`) VALUES (2, 'bbb')`) +// // await sessionTransaction.commit() +// // } +// // +// // await Promise.all([ +// // first(), +// // second() +// // ]) +// } +// +// main() diff --git a/test/__spanner-test/start_spanner.bash b/test/__spanner-test/start_spanner.bash new file mode 100644 index 0000000000..01df1df956 --- /dev/null +++ b/test/__spanner-test/start_spanner.bash @@ -0,0 +1,22 @@ +#!/bin/bash + +set -m + +gcloud beta emulators spanner start --host-port=0.0.0.0:9010 & + +# configure gcloud cli to connect to emulator +gcloud config set auth/disable_credentials true +gcloud config set project test-project +gcloud config set api_endpoint_overrides/spanner http://localhost:9020/ + +# create spanner instance +gcloud spanner instances create test-instance \ + --config=emulator-config \ + --description="Test Instance" \ + --nodes=1 + +# create spanner database with the given schema +gcloud spanner databases create test-db \ + --instance=test-instance + +fg %1 diff --git a/test/functional/cache/custom-cache-provider.ts b/test/functional/cache/custom-cache-provider.ts index dbb41c632d..c73e3db2ad 100644 --- a/test/functional/cache/custom-cache-provider.ts +++ b/test/functional/cache/custom-cache-provider.ts @@ -29,6 +29,10 @@ describe("custom cache provider", () => { it("should be used instead of built-ins", () => Promise.all( connections.map(async (connection) => { + if (connection.driver.options.type === "spanner") { + return + } + const queryResultCache: any = connection.queryResultCache expect(queryResultCache).to.have.property( "queryResultCacheTable", @@ -43,6 +47,9 @@ describe("custom cache provider", () => { it("should cache results properly", () => Promise.all( connections.map(async (connection) => { + if (connection.driver.options.type === "spanner") { + return + } // first prepare data - insert users const user1 = new User() user1.firstName = "Timber" @@ -108,6 +115,10 @@ describe("custom cache provider", () => { it("should cache results with pagination enabled properly", () => Promise.all( connections.map(async (connection) => { + if (connection.driver.options.type === "spanner") { + return + } + // first prepare data - insert users const user1 = new User() user1.firstName = "Timber" @@ -185,6 +196,10 @@ describe("custom cache provider", () => { it("should cache results with custom id and duration supplied", () => Promise.all( connections.map(async (connection) => { + if (connection.driver.options.type === "spanner") { + return + } + // first prepare data - insert users const user1 = new User() user1.firstName = "Timber" @@ -265,6 +280,10 @@ describe("custom cache provider", () => { it("should cache results with custom id and duration supplied", () => Promise.all( connections.map(async (connection) => { + if (connection.driver.options.type === "spanner") { + return + } + // first prepare data - insert users const user1 = new User() user1.firstName = "Timber" diff --git a/test/functional/columns/update-insert/columns-update-insert.ts b/test/functional/columns/update-insert/columns-update-insert.ts index d4e8273f8e..7a4b7d4af7 100644 --- a/test/functional/columns/update-insert/columns-update-insert.ts +++ b/test/functional/columns/update-insert/columns-update-insert.ts @@ -22,6 +22,10 @@ describe("columns > update and insert control", () => { it("should respect column update and insert properties", () => Promise.all( connections.map(async (connection) => { + if (connection.driver.options.type === "spanner") { + return + } + const postRepository = connection.getRepository(Post) // create and save a post first diff --git a/test/functional/database-schema/column-types/spanner/column-types-spanner.ts b/test/functional/database-schema/column-types/spanner/column-types-spanner.ts new file mode 100644 index 0000000000..b3aef611c1 --- /dev/null +++ b/test/functional/database-schema/column-types/spanner/column-types-spanner.ts @@ -0,0 +1,160 @@ +import "reflect-metadata" +import { DataSource } from "../../../../../src" +import { Post } from "./entity/Post" +import { + closeTestingConnections, + createTestingConnections, + reloadTestingDatabases, +} from "../../../../utils/test-utils" +import { PostWithoutTypes } from "./entity/PostWithoutTypes" +import { PostWithOptions } from "./entity/PostWithOptions" + +describe("database schema > column types > spanner", () => { + let connections: DataSource[] + before(async () => { + connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + enabledDrivers: ["spanner"], + }) + }) + beforeEach(() => reloadTestingDatabases(connections)) + after(() => closeTestingConnections(connections)) + + it("all types should work correctly - persist and hydrate", () => + Promise.all( + connections.map(async (connection) => { + const postRepository = connection.getRepository(Post) + const queryRunner = connection.createQueryRunner() + const table = await queryRunner.getTable("post") + await queryRunner.release() + + const post = new Post() + post.id = 1 + post.name = "Post" + post.int64 = 2147483647 + post.string = "This is string" + post.bytes = Buffer.from("This is bytes") + post.float64 = 10.53 + post.numeric = "10" + post.bool = true + post.date = "2022-03-16" + post.timestamp = new Date() + post.json = { param: "VALUE" } + post.array = ["A", "B", "C"] + await postRepository.save(post) + + const loadedPost = (await postRepository.findOneBy({ id: 1 }))! + loadedPost.id.should.be.equal(post.id) + loadedPost.name.should.be.equal(post.name) + loadedPost.int64.should.be.equal(post.int64) + loadedPost.string.should.be.equal(post.string) + loadedPost.bytes + .toString() + .should.be.equal(post.bytes.toString()) + loadedPost.float64.should.be.equal(post.float64) + loadedPost.numeric.should.be.equal(post.numeric) + loadedPost.bool.should.be.equal(post.bool) + loadedPost.date.should.be.equal(post.date) + loadedPost.timestamp + .valueOf() + .should.be.equal(post.timestamp.valueOf()) + loadedPost.json.should.be.eql(post.json) + loadedPost.array[0].should.be.equal(post.array[0]) + loadedPost.array[1].should.be.equal(post.array[1]) + loadedPost.array[2].should.be.equal(post.array[2]) + + table!.findColumnByName("id")!.type.should.be.equal("int64") + table!.findColumnByName("name")!.type.should.be.equal("string") + table!.findColumnByName("int64")!.type.should.be.equal("int64") + table! + .findColumnByName("string")! + .type.should.be.equal("string") + table!.findColumnByName("bytes")!.type.should.be.equal("bytes") + table! + .findColumnByName("float64")! + .type.should.be.equal("float64") + table! + .findColumnByName("numeric")! + .type.should.be.equal("numeric") + table!.findColumnByName("date")!.type.should.be.equal("date") + table!.findColumnByName("bool")!.type.should.be.equal("bool") + table!.findColumnByName("date")!.type.should.be.equal("date") + table! + .findColumnByName("timestamp")! + .type.should.be.equal("timestamp") + table!.findColumnByName("json")!.type.should.be.equal("json") + table!.findColumnByName("array")!.type.should.be.equal("string") + table!.findColumnByName("array")!.isArray.should.be.true + }), + )) + + it("all types should work correctly - persist and hydrate when options are specified on columns", () => + Promise.all( + connections.map(async (connection) => { + const postRepository = connection.getRepository(PostWithOptions) + const queryRunner = connection.createQueryRunner() + const table = await queryRunner.getTable("post_with_options") + await queryRunner.release() + + const post = new PostWithOptions() + post.id = 1 + post.string = "This is string" + post.bytes = Buffer.from("This is bytes") + await postRepository.save(post) + + const loadedPost = (await postRepository.findOneBy({ id: 1 }))! + loadedPost.id.should.be.equal(post.id) + loadedPost.string.should.be.equal(post.string) + loadedPost.bytes + .toString() + .should.be.equal(post.bytes.toString()) + + table!.findColumnByName("id")!.type.should.be.equal("int64") + table! + .findColumnByName("string")! + .type.should.be.equal("string") + table!.findColumnByName("string")!.length!.should.be.equal("50") + table!.findColumnByName("bytes")!.type.should.be.equal("bytes") + table!.findColumnByName("bytes")!.length!.should.be.equal("50") + }), + )) + + it("all types should work correctly - persist and hydrate when types are not specified on columns", () => + Promise.all( + connections.map(async (connection) => { + const postRepository = + connection.getRepository(PostWithoutTypes) + const queryRunner = connection.createQueryRunner() + const table = await queryRunner.getTable("post_without_types") + await queryRunner.release() + + const post = new PostWithoutTypes() + post.id = 1 + post.name = "Post" + post.bool = true + post.bytes = Buffer.from("A") + post.timestamp = new Date() + post.timestamp.setMilliseconds(0) + await postRepository.save(post) + + const loadedPost = (await postRepository.findOneBy({ id: 1 }))! + loadedPost.id.should.be.equal(post.id) + loadedPost.name.should.be.equal(post.name) + loadedPost.bool.should.be.equal(post.bool) + loadedPost.bytes + .toString() + .should.be.equal(post.bytes.toString()) + loadedPost.timestamp + .valueOf() + .should.be.equal(post.timestamp.valueOf()) + + table!.findColumnByName("id")!.type.should.be.equal("int64") + table!.findColumnByName("name")!.type.should.be.equal("string") + table!.findColumnByName("bool")!.type.should.be.equal("bool") + table!.findColumnByName("bytes")!.type.should.be.equal("bytes") + table! + .findColumnByName("timestamp")! + .type.should.be.equal("timestamp") + }), + )) +}) diff --git a/test/functional/database-schema/column-types/spanner/entity/Post.ts b/test/functional/database-schema/column-types/spanner/entity/Post.ts new file mode 100644 index 0000000000..4710ceebb5 --- /dev/null +++ b/test/functional/database-schema/column-types/spanner/entity/Post.ts @@ -0,0 +1,68 @@ +import { Column, Entity, PrimaryColumn } from "../../../../../../src" + +@Entity() +export class Post { + @PrimaryColumn() + id: number + + @Column() + name: string + + // ------------------------------------------------------------------------- + // Integer Types + // ------------------------------------------------------------------------- + + @Column("int64") + int64: number + + // ------------------------------------------------------------------------- + // Character Types + // ------------------------------------------------------------------------- + + @Column("string") + string: string + + // ------------------------------------------------------------------------- + // Float Types + // ------------------------------------------------------------------------- + + @Column("float64") + float64: number + + // ------------------------------------------------------------------------- + // Binary Types + // ------------------------------------------------------------------------- + + @Column("bytes") + bytes: Buffer + + // ------------------------------------------------------------------------- + // Numeric Types + // ------------------------------------------------------------------------- + + @Column("numeric") + numeric: string + + // ------------------------------------------------------------------------- + // Date Types + // ------------------------------------------------------------------------- + + @Column("date") + date: string + + @Column("timestamp") + timestamp: Date + + // ------------------------------------------------------------------------- + // Other Types + // ------------------------------------------------------------------------- + + @Column("bool") + bool: boolean + + @Column("json") + json: Object + + @Column("string", { array: true }) + array: string[] +} diff --git a/test/functional/database-schema/column-types/spanner/entity/PostWithOptions.ts b/test/functional/database-schema/column-types/spanner/entity/PostWithOptions.ts new file mode 100644 index 0000000000..008c5c0fd0 --- /dev/null +++ b/test/functional/database-schema/column-types/spanner/entity/PostWithOptions.ts @@ -0,0 +1,13 @@ +import { Column, Entity, PrimaryColumn } from "../../../../../../src" + +@Entity() +export class PostWithOptions { + @PrimaryColumn() + id: number + + @Column({ length: 50 }) + string: string + + @Column({ length: 50 }) + bytes: Buffer +} diff --git a/test/functional/database-schema/column-types/spanner/entity/PostWithoutTypes.ts b/test/functional/database-schema/column-types/spanner/entity/PostWithoutTypes.ts new file mode 100644 index 0000000000..7d375bcbf2 --- /dev/null +++ b/test/functional/database-schema/column-types/spanner/entity/PostWithoutTypes.ts @@ -0,0 +1,19 @@ +import { Column, Entity, PrimaryColumn } from "../../../../../../src" + +@Entity() +export class PostWithoutTypes { + @PrimaryColumn() + id: number + + @Column() + name: string + + @Column() + bool: boolean + + @Column() + bytes: Buffer + + @Column() + timestamp: Date +} diff --git a/test/functional/driver/convert-to-entity/entity/Post.ts b/test/functional/driver/convert-to-entity/entity/Post.ts index 1593658690..8b7b13be97 100644 --- a/test/functional/driver/convert-to-entity/entity/Post.ts +++ b/test/functional/driver/convert-to-entity/entity/Post.ts @@ -4,7 +4,7 @@ import { PrimaryColumn } from "../../../../../src/decorator/columns/PrimaryColum @Entity() export class Post { - @PrimaryColumn("int") + @PrimaryColumn() id: number @Column({ nullable: true }) diff --git a/test/functional/embedded/embedded-many-to-many-case1/embedded-many-to-many-case1.ts b/test/functional/embedded/embedded-many-to-many-case1/embedded-many-to-many-case1.ts index 7a703ecbe0..37c3964790 100644 --- a/test/functional/embedded/embedded-many-to-many-case1/embedded-many-to-many-case1.ts +++ b/test/functional/embedded/embedded-many-to-many-case1/embedded-many-to-many-case1.ts @@ -432,7 +432,7 @@ describe("embedded > embedded-many-to-many-case1", () => { const loadedUsers2 = (await connection .getRepository(User) - .find())! + .find({ order: { name: "ASC" } }))! expect(loadedUsers2.length).to.be.equal(2) expect(loadedUsers2[0].name).to.be.equal("Bob") expect(loadedUsers2[1].name).to.be.equal("Clara") diff --git a/test/functional/embedded/embedded-many-to-many-case2/embedded-many-to-many-case2.ts b/test/functional/embedded/embedded-many-to-many-case2/embedded-many-to-many-case2.ts index 8b72ecde0e..e1a787eb85 100644 --- a/test/functional/embedded/embedded-many-to-many-case2/embedded-many-to-many-case2.ts +++ b/test/functional/embedded/embedded-many-to-many-case2/embedded-many-to-many-case2.ts @@ -240,7 +240,7 @@ describe("embedded > embedded-many-to-many-case2", () => { const loadedUsers2 = (await connection .getRepository(User) - .find())! + .find({ order: { name: "ASC" } }))! expect(loadedUsers2.length).to.be.equal(2) expect(loadedUsers2[0].name).to.be.equal("Bob") expect(loadedUsers2[1].name).to.be.equal("Clara") diff --git a/test/functional/embedded/embedded-many-to-many-case3/embedded-many-to-many-case3.ts b/test/functional/embedded/embedded-many-to-many-case3/embedded-many-to-many-case3.ts index 78d0af951f..09ecf300b8 100644 --- a/test/functional/embedded/embedded-many-to-many-case3/embedded-many-to-many-case3.ts +++ b/test/functional/embedded/embedded-many-to-many-case3/embedded-many-to-many-case3.ts @@ -428,7 +428,9 @@ describe("embedded > embedded-many-to-many-case3", () => { await connection.getRepository(User).remove(loadedUser!) - loadedUsers = (await connection.getRepository(User).find())! + loadedUsers = (await connection + .getRepository(User) + .find({ order: { name: "ASC" } }))! expect(loadedUsers.length).to.be.equal(2) expect(loadedUsers[0].name).to.be.equal("Bob") expect(loadedUsers[1].name).to.be.equal("Clara") diff --git a/test/functional/embedded/embedded-many-to-many-case4/embedded-many-to-many-case4.ts b/test/functional/embedded/embedded-many-to-many-case4/embedded-many-to-many-case4.ts index 6706709a69..c26845ff3b 100644 --- a/test/functional/embedded/embedded-many-to-many-case4/embedded-many-to-many-case4.ts +++ b/test/functional/embedded/embedded-many-to-many-case4/embedded-many-to-many-case4.ts @@ -445,7 +445,9 @@ describe("embedded > embedded-many-to-many-case4", () => { await connection.getRepository(User).remove(loadedUser!) - loadedUsers = (await connection.getRepository(User).find())! + loadedUsers = (await connection + .getRepository(User) + .find({ order: { name: "ASC" } }))! expect(loadedUsers.length).to.be.equal(2) expect(loadedUsers[0].name).to.be.equal("Bob") expect(loadedUsers[1].name).to.be.equal("Clara") diff --git a/test/functional/embedded/embedded-many-to-many-case5/embedded-many-to-many-case5.ts b/test/functional/embedded/embedded-many-to-many-case5/embedded-many-to-many-case5.ts index 7ce18eabc9..6b36f1dfd1 100644 --- a/test/functional/embedded/embedded-many-to-many-case5/embedded-many-to-many-case5.ts +++ b/test/functional/embedded/embedded-many-to-many-case5/embedded-many-to-many-case5.ts @@ -445,7 +445,9 @@ describe("embedded > embedded-many-to-many-case5", () => { await connection.getRepository(User).remove(loadedUser!) - loadedUsers = (await connection.getRepository(User).find())! + loadedUsers = (await connection + .getRepository(User) + .find({ order: { name: "ASC" } }))! expect(loadedUsers.length).to.be.equal(2) expect(loadedUsers[0].name).to.be.equal("Bob") expect(loadedUsers[1].name).to.be.equal("Clara") diff --git a/test/functional/embedded/embedded-many-to-one-case2/embedded-many-to-one-case2.ts b/test/functional/embedded/embedded-many-to-one-case2/embedded-many-to-one-case2.ts index 9302eccd6f..3d3947289b 100644 --- a/test/functional/embedded/embedded-many-to-one-case2/embedded-many-to-one-case2.ts +++ b/test/functional/embedded/embedded-many-to-one-case2/embedded-many-to-one-case2.ts @@ -184,7 +184,9 @@ describe("embedded > embedded-many-to-one-case2", () => { await connection.getRepository(User).remove(loadedUser!) - loadedUsers = (await connection.getRepository(User).find())! + loadedUsers = (await connection + .getRepository(User) + .find({ order: { name: "ASC" } }))! expect(loadedUsers.length).to.be.equal(1) expect(loadedUsers[0].name).to.be.equal("Bob") }), diff --git a/test/functional/embedded/embedded-one-to-one/embedded-one-to-one.ts b/test/functional/embedded/embedded-one-to-one/embedded-one-to-one.ts index 2e4d88142a..605ed5877a 100644 --- a/test/functional/embedded/embedded-one-to-one/embedded-one-to-one.ts +++ b/test/functional/embedded/embedded-one-to-one/embedded-one-to-one.ts @@ -346,7 +346,9 @@ describe("embedded > embedded-one-to-one", () => { await connection.getRepository(User).remove(loadedUser!) - loadedUsers = (await connection.getRepository(User).find())! + loadedUsers = (await connection + .getRepository(User) + .find({ order: { name: "ASC" } }))! expect(loadedUsers.length).to.be.equal(1) expect(loadedUsers[0].name).to.be.equal("Bob") }), diff --git a/test/functional/entity-model/entity-model.ts b/test/functional/entity-model/entity-model.ts index 392a27dcf3..521dfe9eb8 100644 --- a/test/functional/entity-model/entity-model.ts +++ b/test/functional/entity-model/entity-model.ts @@ -25,6 +25,7 @@ describe("entity-model", () => { Post.useDataSource(connection) // change connection each time because of AR specifics const post = Post.create() + post.id = 1 post.title = "About ActiveRecord" post.text = "Huge discussion how good or bad ActiveRecord is." await post.save() @@ -52,16 +53,18 @@ describe("entity-model", () => { const externalId = "external-entity" - await Post.upsert({ externalId, title: "External post" }, [ - "externalId", - ]) + await Post.upsert( + { externalId, id: 1, title: "External post" }, + ["externalId"], + ) const upsertInsertedExternalPost = await Post.findOneByOrFail({ externalId, }) - await Post.upsert({ externalId, title: "External post 2" }, [ - "externalId", - ]) + await Post.upsert( + { externalId, id: 1, title: "External post 2" }, + ["externalId"], + ) const upsertUpdatedExternalPost = await Post.findOneByOrFail({ externalId, }) @@ -89,6 +92,7 @@ describe("entity-model", () => { await category.save() const post = Post.create() + post.id = 1 post.title = "About ActiveRecord" post.categories = [category] await post.save() @@ -126,6 +130,7 @@ describe("entity-model", () => { Category.useDataSource(connection) const post1 = Post.create() + post1.id = 1 post1.title = "About ActiveRecord 1" post1.externalId = "some external id 1" await post1.save() @@ -146,6 +151,7 @@ describe("entity-model", () => { }) const post2 = Post.create() + post2.id = 2 post2.title = "About ActiveRecord 2" post2.externalId = "some external id 2" await post2.save() diff --git a/test/functional/entity-model/entity/Post.ts b/test/functional/entity-model/entity/Post.ts index 48c8eceeb5..281651a581 100644 --- a/test/functional/entity-model/entity/Post.ts +++ b/test/functional/entity-model/entity/Post.ts @@ -4,13 +4,13 @@ import { Entity, JoinTable, ManyToMany, - PrimaryGeneratedColumn, + PrimaryColumn, } from "../../../../src" import { Category } from "./Category" @Entity() export class Post extends BaseEntity { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @Column({ diff --git a/test/functional/entity-schema/basic/entity-schema-basic.ts b/test/functional/entity-schema/basic/entity-schema-basic.ts index 24fcead5f0..68f02df77e 100644 --- a/test/functional/entity-schema/basic/entity-schema-basic.ts +++ b/test/functional/entity-schema/basic/entity-schema-basic.ts @@ -24,6 +24,7 @@ describe("entity schemas > basic functionality", () => { connections.map(async (connection) => { const postRepository = connection.getRepository(PostEntity) const post = postRepository.create({ + id: 1, title: "First Post", text: "About first post", }) diff --git a/test/functional/entity-schema/basic/entity/CategoryEntity.ts b/test/functional/entity-schema/basic/entity/CategoryEntity.ts index 22f289bdea..4787704424 100644 --- a/test/functional/entity-schema/basic/entity/CategoryEntity.ts +++ b/test/functional/entity-schema/basic/entity/CategoryEntity.ts @@ -7,7 +7,6 @@ export const CategoryEntity = new EntitySchema({ id: { type: Number, primary: true, - generated: true, }, name: { type: String, diff --git a/test/functional/entity-schema/basic/entity/PostEntity.ts b/test/functional/entity-schema/basic/entity/PostEntity.ts index 664cc6210e..27e2276c3b 100644 --- a/test/functional/entity-schema/basic/entity/PostEntity.ts +++ b/test/functional/entity-schema/basic/entity/PostEntity.ts @@ -7,7 +7,6 @@ export const PostEntity = new EntitySchema({ id: { type: Number, primary: true, - generated: true, }, title: { type: String, diff --git a/test/functional/entity-schema/checks/checks-basic.ts b/test/functional/entity-schema/checks/checks-basic.ts index 518d586147..9c4f006369 100644 --- a/test/functional/entity-schema/checks/checks-basic.ts +++ b/test/functional/entity-schema/checks/checks-basic.ts @@ -6,30 +6,59 @@ import { } from "../../../utils/test-utils" import { DataSource } from "../../../../src/data-source/DataSource" import { PersonSchema } from "./entity/Person" -import { DriverUtils } from "../../../../src/driver/DriverUtils" +import { PersonSchema2 } from "./entity/Person2" describe("entity-schema > checks", () => { - let connections: DataSource[] - before( - async () => - (connections = await createTestingConnections({ - entities: [PersonSchema], - })), - ) - beforeEach(() => reloadTestingDatabases(connections)) - after(() => closeTestingConnections(connections)) + describe("entity-schema > checks > postgres, cockroachdb, oracle, mssql", () => { + let connections: DataSource[] + before( + async () => + (connections = await createTestingConnections({ + entities: [PersonSchema], + enabledDrivers: [ + "postgres", + "cockroachdb", + "oracle", + "mssql", + ], + })), + ) + beforeEach(() => reloadTestingDatabases(connections)) + after(() => closeTestingConnections(connections)) - it("should create a check constraints", () => - Promise.all( - connections.map(async (connection) => { - // Mysql does not support check constraints. - if (DriverUtils.isMySQLFamily(connection.driver)) return + it("should create a check constraints", () => + Promise.all( + connections.map(async (connection) => { + const queryRunner = connection.createQueryRunner() + const table = await queryRunner.getTable("person") + await queryRunner.release() - const queryRunner = connection.createQueryRunner() - const table = await queryRunner.getTable("person") - await queryRunner.release() + table!.checks.length.should.be.equal(2) + }), + )) + }) - table!.checks.length.should.be.equal(2) - }), - )) + describe("entity-schema > checks > spanner", () => { + let connections: DataSource[] + before( + async () => + (connections = await createTestingConnections({ + entities: [PersonSchema2], + enabledDrivers: ["spanner"], + })), + ) + beforeEach(() => reloadTestingDatabases(connections)) + after(() => closeTestingConnections(connections)) + + it("should create a check constraints", () => + Promise.all( + connections.map(async (connection) => { + const queryRunner = connection.createQueryRunner() + const table = await queryRunner.getTable("person") + await queryRunner.release() + + table!.checks.length.should.be.equal(2) + }), + )) + }) }) diff --git a/test/functional/entity-schema/checks/entity/Person.ts b/test/functional/entity-schema/checks/entity/Person.ts index d1040af38d..ee5649ae18 100644 --- a/test/functional/entity-schema/checks/entity/Person.ts +++ b/test/functional/entity-schema/checks/entity/Person.ts @@ -5,7 +5,7 @@ export const PersonSchema = new EntitySchema({ columns: { Id: { primary: true, - type: "int", + type: Number, generated: "increment", }, FirstName: { diff --git a/test/functional/entity-schema/checks/entity/Person2.ts b/test/functional/entity-schema/checks/entity/Person2.ts new file mode 100644 index 0000000000..425312ecd3 --- /dev/null +++ b/test/functional/entity-schema/checks/entity/Person2.ts @@ -0,0 +1,29 @@ +import { EntitySchema } from "../../../../../src/index" + +export const PersonSchema2 = new EntitySchema({ + name: "Person", + columns: { + Id: { + primary: true, + type: Number, + generated: "increment", + }, + FirstName: { + type: String, + length: 30, + }, + LastName: { + type: String, + length: 50, + nullable: false, + }, + Age: { + type: Number, + nullable: false, + }, + }, + checks: [ + { expression: `\`FirstName\` <> 'John' AND \`LastName\` <> 'Doe'` }, + { expression: `\`Age\` > 18` }, + ], +}) diff --git a/test/functional/entity-schema/exclusions/exclusions-basic.ts b/test/functional/entity-schema/exclusions/exclusions-basic.ts index 6b7f6a86a8..52af78e20d 100644 --- a/test/functional/entity-schema/exclusions/exclusions-basic.ts +++ b/test/functional/entity-schema/exclusions/exclusions-basic.ts @@ -13,6 +13,7 @@ describe("entity-schema > exclusions", () => { async () => (connections = await createTestingConnections({ entities: [MeetingSchema], + enabledDrivers: ["postgres"], })), ) beforeEach(() => reloadTestingDatabases(connections)) @@ -21,9 +22,6 @@ describe("entity-schema > exclusions", () => { it("should create an exclusion constraint", () => Promise.all( connections.map(async (connection) => { - // Only PostgreSQL supports exclusion constraints. - if (!(connection.driver.options.type === "postgres")) return - const queryRunner = connection.createQueryRunner() const table = await queryRunner.getTable("meeting") await queryRunner.release() diff --git a/test/functional/entity-schema/indices/basic/entity/Person.ts b/test/functional/entity-schema/indices/basic/entity/Person.ts index 128f9671f4..e934c41116 100644 --- a/test/functional/entity-schema/indices/basic/entity/Person.ts +++ b/test/functional/entity-schema/indices/basic/entity/Person.ts @@ -5,7 +5,7 @@ export const PersonSchema = new EntitySchema({ columns: { Id: { primary: true, - type: "int", + type: Number, generated: "increment", }, FirstName: { diff --git a/test/functional/entity-schema/uniques/entity/Person.ts b/test/functional/entity-schema/uniques/entity/Person.ts index 424b04aacd..73e697db8f 100644 --- a/test/functional/entity-schema/uniques/entity/Person.ts +++ b/test/functional/entity-schema/uniques/entity/Person.ts @@ -5,7 +5,7 @@ export const PersonSchema = new EntitySchema({ columns: { Id: { primary: true, - type: "int", + type: Number, generated: "increment", }, FirstName: { diff --git a/test/functional/entity-schema/uniques/uniques-basic.ts b/test/functional/entity-schema/uniques/uniques-basic.ts index 34cb6eef1b..998e718b20 100644 --- a/test/functional/entity-schema/uniques/uniques-basic.ts +++ b/test/functional/entity-schema/uniques/uniques-basic.ts @@ -29,7 +29,8 @@ describe("entity-schema > uniques", () => { if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { expect(table!.indices.length).to.be.equal(1) expect(table!.indices[0].name).to.be.equal("UNIQUE_TEST") diff --git a/test/functional/entity-subscriber/transaction-flow/entity-subscriber-transaction-flow.ts b/test/functional/entity-subscriber/transaction-flow/entity-subscriber-transaction-flow.ts index 99d7eff2f2..4834abcbb8 100644 --- a/test/functional/entity-subscriber/transaction-flow/entity-subscriber-transaction-flow.ts +++ b/test/functional/entity-subscriber/transaction-flow/entity-subscriber-transaction-flow.ts @@ -58,7 +58,11 @@ describe("entity subscriber > transaction flow", () => { it("transactionStart", async () => { for (let connection of connections) { - if (connection.driver.options.type === "mssql") return + if ( + connection.driver.options.type === "mssql" || + connection.driver.options.type === "spanner" + ) + return beforeTransactionStart.resetHistory() afterTransactionStart.resetHistory() @@ -140,7 +144,11 @@ describe("entity subscriber > transaction flow", () => { it("transactionCommit", async () => { for (let connection of connections) { - if (connection.driver.options.type === "mssql") return + if ( + connection.driver.options.type === "mssql" || + connection.driver.options.type === "spanner" + ) + return beforeTransactionCommit.resetHistory() afterTransactionCommit.resetHistory() @@ -204,7 +212,11 @@ describe("entity subscriber > transaction flow", () => { it("transactionRollback", async () => { for (let connection of connections) { - if (connection.driver.options.type === "mssql") return + if ( + connection.driver.options.type === "mssql" || + connection.driver.options.type === "spanner" + ) + return beforeTransactionRollback.resetHistory() afterTransactionRollback.resetHistory() diff --git a/test/functional/find-options/basic-usage/entity/Author.ts b/test/functional/find-options/basic-usage/entity/Author.ts index 7772e2d7bf..6e7b3545b5 100644 --- a/test/functional/find-options/basic-usage/entity/Author.ts +++ b/test/functional/find-options/basic-usage/entity/Author.ts @@ -1,14 +1,9 @@ -import { - Column, - Entity, - OneToMany, - PrimaryGeneratedColumn, -} from "../../../../../src" +import { Column, Entity, OneToMany, PrimaryColumn } from "../../../../../src" import { Photo } from "./Photo" @Entity() export class Author { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @Column() diff --git a/test/functional/find-options/basic-usage/entity/Photo.ts b/test/functional/find-options/basic-usage/entity/Photo.ts index e26771f905..1f0a68b5d7 100644 --- a/test/functional/find-options/basic-usage/entity/Photo.ts +++ b/test/functional/find-options/basic-usage/entity/Photo.ts @@ -1,14 +1,9 @@ -import { - Column, - Entity, - ManyToOne, - PrimaryGeneratedColumn, -} from "../../../../../src" +import { Column, Entity, ManyToOne, PrimaryColumn } from "../../../../../src" import { Author } from "./Author" @Entity() export class Photo { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @Column() diff --git a/test/functional/find-options/basic-usage/entity/Post.ts b/test/functional/find-options/basic-usage/entity/Post.ts index 8411eb1524..fbd7f1fb59 100644 --- a/test/functional/find-options/basic-usage/entity/Post.ts +++ b/test/functional/find-options/basic-usage/entity/Post.ts @@ -4,7 +4,7 @@ import { JoinTable, ManyToMany, ManyToOne, - PrimaryGeneratedColumn, + PrimaryColumn, } from "../../../../../src" import { Tag } from "./Tag" import { Author } from "./Author" @@ -12,7 +12,7 @@ import { Counters } from "./Counters" @Entity() export class Post { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @Column() diff --git a/test/functional/find-options/basic-usage/entity/Tag.ts b/test/functional/find-options/basic-usage/entity/Tag.ts index b6902d0c06..b9a5309154 100644 --- a/test/functional/find-options/basic-usage/entity/Tag.ts +++ b/test/functional/find-options/basic-usage/entity/Tag.ts @@ -1,14 +1,9 @@ -import { - Column, - Entity, - ManyToMany, - PrimaryGeneratedColumn, -} from "../../../../../src" +import { Column, Entity, ManyToMany, PrimaryColumn } from "../../../../../src" import { Post } from "./Post" @Entity() export class Tag { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @Column() diff --git a/test/functional/find-options/basic-usage/find-options-order.ts b/test/functional/find-options/basic-usage/find-options-order.ts index 9bf30097ab..ca072ac620 100644 --- a/test/functional/find-options/basic-usage/find-options-order.ts +++ b/test/functional/find-options/basic-usage/find-options-order.ts @@ -393,6 +393,7 @@ describe("find options > order", () => { }, }, order: { + id: "asc", author: { id: "desc", }, diff --git a/test/functional/find-options/basic-usage/find-options-test-utils.ts b/test/functional/find-options/basic-usage/find-options-test-utils.ts index ed09093781..9748883d4d 100644 --- a/test/functional/find-options/basic-usage/find-options-test-utils.ts +++ b/test/functional/find-options/basic-usage/find-options-test-utils.ts @@ -8,16 +8,19 @@ import { Counters } from "./entity/Counters" export async function prepareData(manager: EntityManager) { const photo1 = new Photo() + photo1.id = 1 photo1.filename = "saw.jpg" photo1.description = "Me and saw" await manager.save(photo1) const photo2 = new Photo() + photo2.id = 2 photo2.filename = "chain.jpg" photo2.description = "Me and chain" await manager.save(photo2) const user1 = new Author() + user1.id = 1 user1.firstName = "Timber" user1.lastName = "Saw" user1.age = 25 @@ -25,6 +28,7 @@ export async function prepareData(manager: EntityManager) { await manager.save(user1) const user2 = new Author() + user2.id = 2 user2.firstName = "Gyro" user2.lastName = "Copter" user2.age = 52 @@ -32,18 +36,22 @@ export async function prepareData(manager: EntityManager) { await manager.save(user2) const tag1 = new Tag() + tag1.id = 1 tag1.name = "category #1" await manager.save(tag1) const tag2 = new Tag() + tag2.id = 2 tag2.name = "category #2" await manager.save(tag2) const tag3 = new Tag() + tag3.id = 3 tag3.name = "category #3" await manager.save(tag3) const post1 = new Post() + post1.id = 1 post1.title = "Post #1" post1.text = "About post #1" post1.author = user1 @@ -54,6 +62,7 @@ export async function prepareData(manager: EntityManager) { await manager.save(post1) const post2 = new Post() + post2.id = 2 post2.title = "Post #2" post2.text = "About post #2" post2.author = user1 @@ -64,6 +73,7 @@ export async function prepareData(manager: EntityManager) { await manager.save(post2) const post3 = new Post() + post3.id = 3 post3.title = "Post #3" post3.text = "About post #3" post3.author = user2 diff --git a/test/functional/find-options/basic-usage/find-options-where.ts b/test/functional/find-options/basic-usage/find-options-where.ts index e7bd99f36e..5bbad4c731 100644 --- a/test/functional/find-options/basic-usage/find-options-where.ts +++ b/test/functional/find-options/basic-usage/find-options-where.ts @@ -16,7 +16,9 @@ describe("find options > where", () => { let connections: DataSource[] before( async () => - (connections = await createTestingConnections({ __dirname })), + (connections = await createTestingConnections({ + __dirname, + })), ) beforeEach(() => reloadTestingDatabases(connections)) after(() => closeTestingConnections(connections)) @@ -207,6 +209,9 @@ describe("find options > where", () => { }, }, }, + order: { + id: "asc", + }, }) .getMany() posts.should.be.eql([ @@ -271,6 +276,9 @@ describe("find options > where", () => { likes: 1, }, }, + order: { + id: "asc", + }, }) .getMany() posts.should.be.eql([ @@ -305,6 +313,9 @@ describe("find options > where", () => { }, }, }, + order: { + id: "asc", + }, }) .getMany() posts.should.be.eql([ @@ -351,6 +362,9 @@ describe("find options > where", () => { }, }, ], + order: { + id: "asc", + }, }) .getMany() posts.should.be.eql([ @@ -426,6 +440,9 @@ describe("find options > where", () => { photos: MoreThan(1), }, }, + order: { + id: "asc", + }, }) .getMany() posts3.should.be.eql([ @@ -461,6 +478,9 @@ describe("find options > where", () => { where: { posts: MoreThan(1), }, + order: { + id: "asc", + }, }) .getMany() tags1.should.be.eql([ @@ -486,6 +506,7 @@ describe("find options > where", () => { await prepareData(connection.manager) const post4 = new Post() + post4.id = 4 post4.title = "Post #4" post4.text = "About post #4" post4.counters = new Counters() @@ -501,6 +522,9 @@ describe("find options > where", () => { firstName: undefined, }, }, + order: { + id: "asc", + }, }) .getMany() posts.should.be.eql([ @@ -538,6 +562,7 @@ describe("find options > where", () => { await prepareData(connection.manager) const post4 = new Post() + post4.id = 4 post4.title = "Post #4" post4.text = "About post #4" post4.counters = new Counters() @@ -550,6 +575,9 @@ describe("find options > where", () => { where: { author: true, }, + order: { + id: "asc", + }, }) .getMany() posts.should.be.eql([ diff --git a/test/functional/persistence/bulk-insert-remove-optimization/bulk-insert-remove-optimization.ts b/test/functional/persistence/bulk-insert-remove-optimization/bulk-insert-remove-optimization.ts index c174490407..7a0dfc0017 100644 --- a/test/functional/persistence/bulk-insert-remove-optimization/bulk-insert-remove-optimization.ts +++ b/test/functional/persistence/bulk-insert-remove-optimization/bulk-insert-remove-optimization.ts @@ -7,7 +7,6 @@ import { createTestingConnections, reloadTestingDatabases, } from "../../../utils/test-utils" -// import {expect} from "chai"; describe("persistence > bulk-insert-remove-optimization", function () { // ------------------------------------------------------------------------- @@ -32,12 +31,15 @@ describe("persistence > bulk-insert-remove-optimization", function () { Promise.all( connections.map(async (connection) => { const category1 = new Category() + category1.id = 1 category1.name = "cat#1" const category2 = new Category() + category2.id = 2 category2.name = "cat#2" const post = new Post() + post.id = 1 post.title = "about post" post.categories = [category1, category2] diff --git a/test/functional/persistence/bulk-insert-remove-optimization/entity/Category.ts b/test/functional/persistence/bulk-insert-remove-optimization/entity/Category.ts index ae72e2379f..495d93901d 100644 --- a/test/functional/persistence/bulk-insert-remove-optimization/entity/Category.ts +++ b/test/functional/persistence/bulk-insert-remove-optimization/entity/Category.ts @@ -1,12 +1,12 @@ import { Entity } from "../../../../../src/decorator/entity/Entity" -import { PrimaryGeneratedColumn } from "../../../../../src/decorator/columns/PrimaryGeneratedColumn" import { Post } from "./Post" import { Column } from "../../../../../src/decorator/columns/Column" import { ManyToMany } from "../../../../../src/decorator/relations/ManyToMany" +import { PrimaryColumn } from "../../../../../src" @Entity() export class Category { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @Column() diff --git a/test/functional/persistence/bulk-insert-remove-optimization/entity/Post.ts b/test/functional/persistence/bulk-insert-remove-optimization/entity/Post.ts index 8a07f1e7a7..4baf0dffc1 100644 --- a/test/functional/persistence/bulk-insert-remove-optimization/entity/Post.ts +++ b/test/functional/persistence/bulk-insert-remove-optimization/entity/Post.ts @@ -1,13 +1,13 @@ import { Category } from "./Category" import { Entity } from "../../../../../src/decorator/entity/Entity" -import { PrimaryGeneratedColumn } from "../../../../../src/decorator/columns/PrimaryGeneratedColumn" import { Column } from "../../../../../src/decorator/columns/Column" import { ManyToMany } from "../../../../../src/decorator/relations/ManyToMany" import { JoinTable } from "../../../../../src/decorator/relations/JoinTable" +import { PrimaryColumn } from "../../../../../src" @Entity() export class Post { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @Column() diff --git a/test/functional/persistence/cascades/cascades-example1/cascades-example1.ts b/test/functional/persistence/cascades/cascades-example1/cascades-example1.ts index 735a92e231..1f99a48310 100644 --- a/test/functional/persistence/cascades/cascades-example1/cascades-example1.ts +++ b/test/functional/persistence/cascades/cascades-example1/cascades-example1.ts @@ -24,11 +24,16 @@ describe("persistence > cascades > example 1", () => { Promise.all( connections.map(async (connection) => { const photo = new Photo() + photo.id = 1 + if (connection.driver.options.type === "spanner") + photo.name = "My photo" const profile = new Profile() + profile.id = 1 profile.photo = photo const user = new User() + user.id = 1 user.name = "Umed" user.profile = profile diff --git a/test/functional/persistence/cascades/cascades-example1/entity/Photo.ts b/test/functional/persistence/cascades/cascades-example1/entity/Photo.ts index 550367e104..ecf2f16229 100644 --- a/test/functional/persistence/cascades/cascades-example1/entity/Photo.ts +++ b/test/functional/persistence/cascades/cascades-example1/entity/Photo.ts @@ -1,10 +1,10 @@ import { Column } from "../../../../../../src/decorator/columns/Column" import { Entity } from "../../../../../../src/decorator/entity/Entity" -import { PrimaryGeneratedColumn } from "../../../../../../src/decorator/columns/PrimaryGeneratedColumn" +import { PrimaryColumn } from "../../../../../../src" @Entity() export class Photo { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @Column({ default: "My photo" }) diff --git a/test/functional/persistence/cascades/cascades-example1/entity/Profile.ts b/test/functional/persistence/cascades/cascades-example1/entity/Profile.ts index dd0e202528..9534cfdfee 100644 --- a/test/functional/persistence/cascades/cascades-example1/entity/Profile.ts +++ b/test/functional/persistence/cascades/cascades-example1/entity/Profile.ts @@ -1,13 +1,13 @@ import { Entity } from "../../../../../../src/decorator/entity/Entity" -import { PrimaryGeneratedColumn } from "../../../../../../src/decorator/columns/PrimaryGeneratedColumn" import { User } from "./User" import { Photo } from "./Photo" import { OneToOne } from "../../../../../../src/decorator/relations/OneToOne" import { JoinColumn } from "../../../../../../src/decorator/relations/JoinColumn" +import { PrimaryColumn } from "../../../../../../src" @Entity() export class Profile { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @OneToOne((type) => User, (user) => user.profile, { diff --git a/test/functional/persistence/cascades/cascades-example1/entity/User.ts b/test/functional/persistence/cascades/cascades-example1/entity/User.ts index 504a454444..34c1812b0e 100644 --- a/test/functional/persistence/cascades/cascades-example1/entity/User.ts +++ b/test/functional/persistence/cascades/cascades-example1/entity/User.ts @@ -1,12 +1,12 @@ import { Column } from "../../../../../../src/decorator/columns/Column" import { Entity } from "../../../../../../src/decorator/entity/Entity" -import { PrimaryGeneratedColumn } from "../../../../../../src/decorator/columns/PrimaryGeneratedColumn" import { Profile } from "./Profile" import { OneToOne } from "../../../../../../src/decorator/relations/OneToOne" +import { PrimaryColumn } from "../../../../../../src" @Entity() export class User { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @Column() diff --git a/test/functional/persistence/cascades/cascades-example2/cascades-example2.ts b/test/functional/persistence/cascades/cascades-example2/cascades-example2.ts index b63363dc12..9cfc625448 100644 --- a/test/functional/persistence/cascades/cascades-example2/cascades-example2.ts +++ b/test/functional/persistence/cascades/cascades-example2/cascades-example2.ts @@ -24,6 +24,9 @@ describe("persistence > cascades > example 2", () => { it("should insert everything by cascades properly", () => Promise.all( connections.map(async (connection) => { + // not supported in Spanner + if (connection.driver.options.type === "spanner") return + const photo = new Photo() const user = new User() diff --git a/test/functional/persistence/entity-updation/persistence-entity-updation.ts b/test/functional/persistence/entity-updation/persistence-entity-updation.ts index 1d5d35b096..5a65b7828e 100644 --- a/test/functional/persistence/entity-updation/persistence-entity-updation.ts +++ b/test/functional/persistence/entity-updation/persistence-entity-updation.ts @@ -54,6 +54,9 @@ describe("persistence > entity updation", () => { it("should update default values after saving", () => Promise.all( connections.map(async (connection) => { + // Spanner does not support DEFAULT values + if (connection.driver.options.type === "spanner") return + const post = new PostDefaultValues() post.title = "Post #1" await connection.manager.save(post) @@ -69,6 +72,9 @@ describe("persistence > entity updation", () => { it("should update special columns after saving", () => Promise.all( connections.map(async (connection) => { + // Spanner does not support DEFAULT values + if (connection.driver.options.type === "spanner") return + const post = new PostSpecialColumns() post.title = "Post #1" await connection.manager.save(post) @@ -95,6 +101,9 @@ describe("persistence > entity updation", () => { it("should update even with embeddeds", () => Promise.all( connections.map(async (connection) => { + // Spanner does not support DEFAULT values + if (connection.driver.options.type === "spanner") return + const post = new PostComplex() post.firstId = 1 post.embed = new PostEmbedded() diff --git a/test/functional/persistence/many-to-one-bi-directional/persistence-many-to-one-bi-directional.ts b/test/functional/persistence/many-to-one-bi-directional/persistence-many-to-one-bi-directional.ts index 4eec034899..39c9702cde 100644 --- a/test/functional/persistence/many-to-one-bi-directional/persistence-many-to-one-bi-directional.ts +++ b/test/functional/persistence/many-to-one-bi-directional/persistence-many-to-one-bi-directional.ts @@ -252,6 +252,9 @@ describe("persistence > many-to-one bi-directional relation", function () { it("should set category's post to NULL when post is removed from the database (database ON DELETE)", () => Promise.all( connections.map(async (connection) => { + // Spanner does not support ON DELETE clause + if (connection.driver.options.type === "spanner") return + const post = new Post(1, "Hello Post") await connection.manager.save(post) diff --git a/test/functional/persistence/many-to-one-uni-directional/persistence-many-to-one-uni-directional.ts b/test/functional/persistence/many-to-one-uni-directional/persistence-many-to-one-uni-directional.ts index 7dab730dc4..e76f9bee16 100644 --- a/test/functional/persistence/many-to-one-uni-directional/persistence-many-to-one-uni-directional.ts +++ b/test/functional/persistence/many-to-one-uni-directional/persistence-many-to-one-uni-directional.ts @@ -252,6 +252,9 @@ describe("persistence > many-to-one uni-directional relation", function () { it("should set category's post to NULL when post is removed from the database (database ON DELETE)", () => Promise.all( connections.map(async (connection) => { + // Spanner does not support ON DELETE clause + if (connection.driver.options.type === "spanner") return + const post = new Post(1, "Hello Post") await connection.manager.save(post) diff --git a/test/functional/persistence/multi-primary-key-on-both-sides/multi-primary-key.ts b/test/functional/persistence/multi-primary-key-on-both-sides/multi-primary-key.ts index 750c3e166e..e01b6d9f76 100644 --- a/test/functional/persistence/multi-primary-key-on-both-sides/multi-primary-key.ts +++ b/test/functional/persistence/multi-primary-key-on-both-sides/multi-primary-key.ts @@ -8,7 +8,7 @@ import { DataSource } from "../../../../src/data-source/DataSource" import { Post } from "./entity/Post" import { Category } from "./entity/Category" -describe("persistence > multi primary keys", () => { +describe("persistence > multi primary keys on both sides", () => { let connections: DataSource[] before( async () => diff --git a/test/functional/persistence/multi-primary-key/entity/Category.ts b/test/functional/persistence/multi-primary-key/entity/Category.ts index 05c66e6fcd..4b1a639845 100644 --- a/test/functional/persistence/multi-primary-key/entity/Category.ts +++ b/test/functional/persistence/multi-primary-key/entity/Category.ts @@ -3,12 +3,10 @@ import { PrimaryColumn } from "../../../../../src/decorator/columns/PrimaryColum import { Column } from "../../../../../src/decorator/columns/Column" import { Post } from "./Post" import { OneToMany } from "../../../../../src/decorator/relations/OneToMany" -import { Generated } from "../../../../../src/decorator/Generated" @Entity() export class Category { - @PrimaryColumn("int") - @Generated() + @PrimaryColumn() categoryId: number @Column() diff --git a/test/functional/persistence/multi-primary-key/multi-primary-key.ts b/test/functional/persistence/multi-primary-key/multi-primary-key.ts index daa837152f..260342c69d 100644 --- a/test/functional/persistence/multi-primary-key/multi-primary-key.ts +++ b/test/functional/persistence/multi-primary-key/multi-primary-key.ts @@ -38,6 +38,7 @@ describe("persistence > multi primary keys", () => { // create first category and post and save them const category1 = new Category() + category1.categoryId = 1 category1.name = "Category saved by cascades #1" category1.posts = [post1] diff --git a/test/functional/persistence/one-to-many/entity/Category.ts b/test/functional/persistence/one-to-many/entity/Category.ts index c04ead2a9b..91bedd8695 100644 --- a/test/functional/persistence/one-to-many/entity/Category.ts +++ b/test/functional/persistence/one-to-many/entity/Category.ts @@ -1,12 +1,12 @@ import { Entity } from "../../../../../src/decorator/entity/Entity" -import { PrimaryGeneratedColumn } from "../../../../../src/decorator/columns/PrimaryGeneratedColumn" import { ManyToOne } from "../../../../../src/decorator/relations/ManyToOne" import { Post } from "./Post" import { Column } from "../../../../../src/decorator/columns/Column" +import { PrimaryColumn } from "../../../../../src" @Entity() export class Category { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @ManyToOne((type) => Post, (post) => post.categories) diff --git a/test/functional/persistence/one-to-many/entity/Post.ts b/test/functional/persistence/one-to-many/entity/Post.ts index 8a3a921abe..4c79e46f67 100644 --- a/test/functional/persistence/one-to-many/entity/Post.ts +++ b/test/functional/persistence/one-to-many/entity/Post.ts @@ -1,19 +1,17 @@ import { Category } from "./Category" import { Entity } from "../../../../../src/decorator/entity/Entity" -import { PrimaryGeneratedColumn } from "../../../../../src/decorator/columns/PrimaryGeneratedColumn" import { OneToMany } from "../../../../../src/decorator/relations/OneToMany" import { Column } from "../../../../../src/decorator/columns/Column" +import { PrimaryColumn } from "../../../../../src" @Entity() export class Post { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @OneToMany((type) => Category, (category) => category.post) categories: Category[] | null - @Column({ - default: "supervalue", - }) + @Column() title: string } diff --git a/test/functional/persistence/one-to-many/persistence-one-to-many.ts b/test/functional/persistence/one-to-many/persistence-one-to-many.ts index bea278fa1e..8ad8f84042 100644 --- a/test/functional/persistence/one-to-many/persistence-one-to-many.ts +++ b/test/functional/persistence/one-to-many/persistence-one-to-many.ts @@ -34,10 +34,12 @@ describe("persistence > one-to-many", function () { const categoryRepository = connection.getRepository(Category) const newCategory = categoryRepository.create() + newCategory.id = 1 newCategory.name = "Animals" await categoryRepository.save(newCategory) const newPost = postRepository.create() + newPost.id = 1 newPost.title = "All about animals" await postRepository.save(newPost) @@ -65,10 +67,12 @@ describe("persistence > one-to-many", function () { const categoryRepository = connection.getRepository(Category) const newCategory = categoryRepository.create() + newCategory.id = 1 newCategory.name = "Animals" await categoryRepository.save(newCategory) const newPost = postRepository.create() + newPost.id = 1 newPost.title = "All about animals" newPost.categories = [newCategory] await postRepository.save(newPost) @@ -94,14 +98,17 @@ describe("persistence > one-to-many", function () { const categoryRepository = connection.getRepository(Category) const firstNewCategory = categoryRepository.create() + firstNewCategory.id = 1 firstNewCategory.name = "Animals" await categoryRepository.save(firstNewCategory) const secondNewCategory = categoryRepository.create() + secondNewCategory.id = 2 secondNewCategory.name = "Insects" await categoryRepository.save(secondNewCategory) const newPost = postRepository.create() + newPost.id = 1 newPost.title = "All about animals" await postRepository.save(newPost) @@ -136,14 +143,17 @@ describe("persistence > one-to-many", function () { const categoryRepository = connection.getRepository(Category) let firstNewCategory = categoryRepository.create() + firstNewCategory.id = 1 firstNewCategory.name = "Animals" await categoryRepository.save(firstNewCategory) let secondNewCategory = categoryRepository.create() + secondNewCategory.id = 2 secondNewCategory.name = "Insects" await categoryRepository.save(secondNewCategory) let newPost = postRepository.create() + newPost.id = 1 newPost.title = "All about animals" await postRepository.save(newPost) @@ -176,14 +186,17 @@ describe("persistence > one-to-many", function () { const categoryRepository = connection.getRepository(Category) let firstNewCategory = categoryRepository.create() + firstNewCategory.id = 1 firstNewCategory.name = "Animals" await categoryRepository.save(firstNewCategory) let secondNewCategory = categoryRepository.create() + secondNewCategory.id = 2 secondNewCategory.name = "Insects" await categoryRepository.save(secondNewCategory) let newPost = postRepository.create() + newPost.id = 1 newPost.title = "All about animals" await postRepository.save(newPost) diff --git a/test/functional/persistence/one-to-one/entity/AccessToken.ts b/test/functional/persistence/one-to-one/entity/AccessToken.ts index a4c20cd31d..6c98ece36f 100644 --- a/test/functional/persistence/one-to-one/entity/AccessToken.ts +++ b/test/functional/persistence/one-to-one/entity/AccessToken.ts @@ -7,7 +7,7 @@ import { Generated } from "../../../../../src/decorator/Generated" @Entity() export class AccessToken { - @PrimaryColumn("int") + @PrimaryColumn() @Generated() primaryKey: number diff --git a/test/functional/persistence/one-to-one/entity/User.ts b/test/functional/persistence/one-to-one/entity/User.ts index 03b518555d..cbe4ceae8e 100644 --- a/test/functional/persistence/one-to-one/entity/User.ts +++ b/test/functional/persistence/one-to-one/entity/User.ts @@ -7,7 +7,7 @@ import { Generated } from "../../../../../src/decorator/Generated" @Entity() export class User { - @PrimaryColumn("int") + @PrimaryColumn() @Generated() primaryKey: number diff --git a/test/functional/persistence/persistence-options/listeners/entity/Post.ts b/test/functional/persistence/persistence-options/listeners/entity/Post.ts index 8fd2cb01e5..e8f22ec88c 100644 --- a/test/functional/persistence/persistence-options/listeners/entity/Post.ts +++ b/test/functional/persistence/persistence-options/listeners/entity/Post.ts @@ -1,12 +1,14 @@ -import { BeforeInsert } from "../../../../../../src/decorator/listeners/BeforeInsert" -import { Entity } from "../../../../../../src/decorator/entity/Entity" -import { PrimaryGeneratedColumn } from "../../../../../../src/decorator/columns/PrimaryGeneratedColumn" -import { Column } from "../../../../../../src/decorator/columns/Column" -import { AfterRemove } from "../../../../../../src/decorator/listeners/AfterRemove" +import { + AfterRemove, + BeforeInsert, + Column, + Entity, + PrimaryColumn, +} from "../../../../../../src" @Entity() export class Post { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @Column() diff --git a/test/functional/persistence/persistence-options/listeners/persistence-options-listeners.ts b/test/functional/persistence/persistence-options/listeners/persistence-options-listeners.ts index 2d8956432d..3a02aed7a5 100644 --- a/test/functional/persistence/persistence-options/listeners/persistence-options-listeners.ts +++ b/test/functional/persistence/persistence-options/listeners/persistence-options-listeners.ts @@ -7,7 +7,6 @@ import { import { Post } from "./entity/Post" import { DataSource } from "../../../../../src/data-source/DataSource" import { PostWithDeleteDateColumn } from "./entity/PostWithDeleteDateColumn" -// import {expect} from "chai"; describe("persistence > persistence options > listeners", () => { // ------------------------------------------------------------------------- @@ -30,6 +29,7 @@ describe("persistence > persistence options > listeners", () => { Promise.all( connections.map(async (connection) => { const post = new Post() + post.id = 1 post.title = "Bakhrom" post.description = "Hello" await connection.manager.save(post) @@ -41,6 +41,7 @@ describe("persistence > persistence options > listeners", () => { Promise.all( connections.map(async (connection) => { const post = new Post() + post.id = 1 post.title = "Bakhrom" post.description = "Hello" await connection.manager.save(post, { listeners: false }) @@ -52,6 +53,7 @@ describe("persistence > persistence options > listeners", () => { Promise.all( connections.map(async (connection) => { const post = new Post() + post.id = 1 post.title = "Bakhrom" post.description = "Hello" await connection.manager.save(post) @@ -64,6 +66,7 @@ describe("persistence > persistence options > listeners", () => { Promise.all( connections.map(async (connection) => { const post = new Post() + post.id = 1 post.title = "Bakhrom" post.description = "Hello" await connection.manager.save(post) diff --git a/test/functional/query-builder/cte/entity/foo.ts b/test/functional/query-builder/cte/entity/foo.ts index 20c0ce1df4..e3fdf762c9 100644 --- a/test/functional/query-builder/cte/entity/foo.ts +++ b/test/functional/query-builder/cte/entity/foo.ts @@ -7,6 +7,6 @@ export class Foo { @PrimaryColumn() id: number - @Column("varchar") + @Column() bar: string } diff --git a/test/functional/query-builder/cte/recursive-cte.ts b/test/functional/query-builder/cte/recursive-cte.ts index f32e0bd42e..0e79a273c6 100644 --- a/test/functional/query-builder/cte/recursive-cte.ts +++ b/test/functional/query-builder/cte/recursive-cte.ts @@ -25,6 +25,9 @@ describe("query builder > cte > recursive", () => { connections .filter(filterByCteCapabilities("enabled")) .map(async (connection) => { + // CTE cannot reference itself in Spanner + if (connection.options.type === "spanner") return + const qb = await connection .createQueryBuilder() .select([]) diff --git a/test/functional/query-builder/cte/simple-cte.ts b/test/functional/query-builder/cte/simple-cte.ts index 96f0b17a5c..f83814d5a2 100644 --- a/test/functional/query-builder/cte/simple-cte.ts +++ b/test/functional/query-builder/cte/simple-cte.ts @@ -35,18 +35,28 @@ describe("query builder > cte > simple", () => { const cteQuery = connection .createQueryBuilder() .select() - .addSelect(`foo.bar`) + .addSelect(`foo.bar`, "bar") .from(Foo, "foo") .where(`foo.bar = :value`, { value: "2" }) + // Spanner does not support column names in CTE + const cteOptions = + connection.driver.options.type === "spanner" + ? undefined + : { + columnNames: ["raz"], + } + const cteSelection = + connection.driver.options.type === "spanner" + ? "qaz.bar" + : "qaz.raz" + const qb = await connection .createQueryBuilder() - .addCommonTableExpression(cteQuery, "qaz", { - columnNames: ["raz"], - }) + .addCommonTableExpression(cteQuery, "qaz", cteOptions) .from("qaz", "qaz") .select([]) - .addSelect("qaz.raz", "raz") + .addSelect(cteSelection, "raz") expect(await qb.getRawMany()).to.deep.equal([{ raz: "2" }]) }), @@ -65,16 +75,26 @@ describe("query builder > cte > simple", () => { const cteQuery = connection .createQueryBuilder() .select() - .addSelect("bar") + .addSelect("bar", "bar") .from(Foo, "foo") .where(`foo.bar = '2'`) + // Spanner does not support column names in CTE + const cteOptions = + connection.driver.options.type === "spanner" + ? undefined + : { + columnNames: ["raz"], + } + const cteSelection = + connection.driver.options.type === "spanner" + ? "qaz.bar" + : "qaz.raz" + const results = await connection .createQueryBuilder(Foo, "foo") - .addCommonTableExpression(cteQuery, "qaz", { - columnNames: ["raz"], - }) - .innerJoin("qaz", "qaz", "qaz.raz = foo.bar") + .addCommonTableExpression(cteQuery, "qaz", cteOptions) + .innerJoin("qaz", "qaz", `${cteSelection} = foo.bar`) .getMany() expect(results).to.have.length(1) @@ -121,21 +141,41 @@ describe("query builder > cte > simple", () => { connections .filter(filterByCteCapabilities("enabled")) .map(async (connection) => { - const results = await connection - .createQueryBuilder() - .select() - .addCommonTableExpression( - ` - SELECT 1 - UNION - SELECT 2 - `, - "cte", - { columnNames: ["foo"] }, - ) - .from("cte", "cte") - .addSelect("foo", "row") - .getRawMany<{ row: any }>() + // Spanner does not support column names in CTE + + let results: { row: any }[] = [] + if (connection.driver.options.type === "spanner") { + results = await connection + .createQueryBuilder() + .select() + .addCommonTableExpression( + ` + SELECT 1 AS foo + UNION ALL + SELECT 2 AS foo + `, + "cte", + ) + .from("cte", "cte") + .addSelect("foo", "row") + .getRawMany<{ row: any }>() + } else { + results = await connection + .createQueryBuilder() + .select() + .addCommonTableExpression( + ` + SELECT 1 + UNION + SELECT 2 + `, + "cte", + { columnNames: ["foo"] }, + ) + .from("cte", "cte") + .addSelect("foo", "row") + .getRawMany<{ row: any }>() + } const [rowWithOne, rowWithTwo] = results diff --git a/test/functional/query-builder/delete/query-builder-delete.ts b/test/functional/query-builder/delete/query-builder-delete.ts index 3fa7b13b19..660ed534f7 100644 --- a/test/functional/query-builder/delete/query-builder-delete.ts +++ b/test/functional/query-builder/delete/query-builder-delete.ts @@ -133,7 +133,8 @@ describe("query builder > delete", () => { const result = await connection .createQueryBuilder() .delete() - .from(User) + .from(User, "user") + .where("name IS NOT NULL") .execute() expect(result.affected).to.equal(2) diff --git a/test/functional/query-builder/entity-updation/entity-updation.ts b/test/functional/query-builder/entity-updation/entity-updation.ts index 6f4ec744e0..b127a1eb6a 100644 --- a/test/functional/query-builder/entity-updation/entity-updation.ts +++ b/test/functional/query-builder/entity-updation/entity-updation.ts @@ -43,6 +43,11 @@ describe("query builder > entity updation", () => { it("should not update entity model after insertion if updateEntity is set to false", () => Promise.all( connections.map(async (connection) => { + // for spanner we skip this test, because it's not possible to do it right considering we faked primary generated column + // for the spanner and we have updateEntity(false) in this test, but we cannot disable subscriber defined in the tests setup + // for the spanner and it updates the entity with it's id anyway + if (connection.driver.options.type === "spanner") return + const post = new Post() post.title = "about entity updation in query builder" diff --git a/test/functional/query-builder/insert/query-builder-insert.ts b/test/functional/query-builder/insert/query-builder-insert.ts index 342d624d3b..738eb62a14 100644 --- a/test/functional/query-builder/insert/query-builder-insert.ts +++ b/test/functional/query-builder/insert/query-builder-insert.ts @@ -51,7 +51,11 @@ describe("query builder > insert", () => { .values({ name: "Muhammad Mirzoev" }) .execute() - const users = await connection.getRepository(User).find() + const users = await connection.getRepository(User).find({ + order: { + id: "ASC", + }, + }) users.should.be.eql([ { id: 1, name: "Alex Messer" }, { id: 2, name: "Dima Zotov" }, @@ -81,7 +85,11 @@ describe("query builder > insert", () => { ]) .execute() - const users = await connection.getRepository(User).find() + const users = await connection.getRepository(User).find({ + order: { + id: "ASC", + }, + }) users.should.be.eql([ { id: 1, name: "Umed Khudoiberdiev" }, { id: 2, name: "Bakhrom Baubekov" }, diff --git a/test/functional/query-builder/join/query-builder-joins.ts b/test/functional/query-builder/join/query-builder-joins.ts index bea6318ce7..c491f0f07c 100644 --- a/test/functional/query-builder/join/query-builder-joins.ts +++ b/test/functional/query-builder/join/query-builder-joins.ts @@ -117,6 +117,7 @@ describe("query builder > joins", () => { .leftJoinAndSelect("post.categories", "categories") .leftJoinAndSelect("categories.images", "images") .where("post.id = :id", { id: 1 }) + .orderBy("post.id, categories.id") .getOne() expect(loadedPost!.tag).to.not.be.undefined diff --git a/test/functional/query-builder/locking/query-builder-locking.ts b/test/functional/query-builder/locking/query-builder-locking.ts index 0395c7f17b..5c7f5069fb 100644 --- a/test/functional/query-builder/locking/query-builder-locking.ts +++ b/test/functional/query-builder/locking/query-builder-locking.ts @@ -35,7 +35,8 @@ describe("query builder > locking", () => { connections.map(async (connection) => { if ( DriverUtils.isSQLiteFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) return @@ -56,7 +57,8 @@ describe("query builder > locking", () => { connections.map(async (connection) => { if ( DriverUtils.isSQLiteFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) return @@ -87,7 +89,8 @@ describe("query builder > locking", () => { connections.map(async (connection) => { if ( DriverUtils.isSQLiteFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) return @@ -371,7 +374,8 @@ describe("query builder > locking", () => { if ( DriverUtils.isSQLiteFamily(connection.driver) || connection.driver.options.type === "cockroachdb" || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) return @@ -414,7 +418,8 @@ describe("query builder > locking", () => { connections.map(async (connection) => { if ( DriverUtils.isSQLiteFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) return @@ -433,7 +438,8 @@ describe("query builder > locking", () => { connections.map(async (connection) => { if ( DriverUtils.isSQLiteFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) return @@ -789,7 +795,8 @@ describe("query builder > locking", () => { connections.map(async (connection) => { if ( DriverUtils.isSQLiteFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) return connection.manager.transaction((entityManager) => { return Promise.all([ diff --git a/test/functional/query-builder/relation-count/relation-count-many-to-many/load-relation-count-and-map-many-to-many.ts b/test/functional/query-builder/relation-count/relation-count-many-to-many/load-relation-count-and-map-many-to-many.ts index 59e161fc43..1331d0db11 100644 --- a/test/functional/query-builder/relation-count/relation-count-many-to-many/load-relation-count-and-map-many-to-many.ts +++ b/test/functional/query-builder/relation-count/relation-count-many-to-many/load-relation-count-and-map-many-to-many.ts @@ -60,6 +60,7 @@ describe("query builder > load-relation-count-and-map > many-to-many", () => { "post.categoryCount", "post.categories", ) + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].categoryCount).to.be.equal(3) @@ -385,6 +386,7 @@ describe("query builder > load-relation-count-and-map > many-to-many", () => { "category.postCount", "category.posts", ) + .addOrderBy("category.id") .getMany() expect(loadedCategories![0].postCount).to.be.equal(3) @@ -516,6 +518,7 @@ describe("query builder > load-relation-count-and-map > many-to-many", () => { isRemoved: true, }), ) + .addOrderBy("category.id") .getMany() expect(loadedCategories![0].postCount).to.be.equal(3) diff --git a/test/functional/query-builder/relation-count/relation-count-one-to-many/load-relation-count-and-map-one-to-many.ts b/test/functional/query-builder/relation-count/relation-count-one-to-many/load-relation-count-and-map-one-to-many.ts index 511229cd6e..8780f3238e 100644 --- a/test/functional/query-builder/relation-count/relation-count-one-to-many/load-relation-count-and-map-one-to-many.ts +++ b/test/functional/query-builder/relation-count/relation-count-one-to-many/load-relation-count-and-map-one-to-many.ts @@ -52,6 +52,7 @@ describe("query builder > load-relation-count-and-map > one-to-many", () => { "post.categoryCount", "post.categories", ) + .addOrderBy("post.id") .getMany() expect(loadedPosts[0]!.categoryCount).to.be.equal(2) @@ -191,6 +192,7 @@ describe("query builder > load-relation-count-and-map > one-to-many", () => { isRemoved: true, }), ) + .addOrderBy("post.id") .getMany() expect(loadedPosts[0]!.categoryCount).to.be.equal(2) diff --git a/test/functional/query-builder/relation-id/many-to-many/multiple-pk/multiple-pk.ts b/test/functional/query-builder/relation-id/many-to-many/multiple-pk/multiple-pk.ts index e35d897d60..56557109ff 100644 --- a/test/functional/query-builder/relation-id/many-to-many/multiple-pk/multiple-pk.ts +++ b/test/functional/query-builder/relation-id/many-to-many/multiple-pk/multiple-pk.ts @@ -69,6 +69,7 @@ describe("query builder > relation-id > many-to-many > multiple-pk", () => { "post.categoryIds", "post.categories", ) + .addOrderBy("post.id") .getMany() expect(loadedPosts[0].categoryIds).to.not.be.eql([]) @@ -151,6 +152,7 @@ describe("query builder > relation-id > many-to-many > multiple-pk", () => { "category.imageIds", "category.images", ) + .addOrderBy("category.id") .getMany() expect(loadedCategories[0].imageIds).to.not.be.eql([]) @@ -231,6 +233,7 @@ describe("query builder > relation-id > many-to-many > multiple-pk", () => { { id: 1, code: 1 }, ), ) + .addOrderBy("post.id") .getMany() expect(loadedPosts[0].categoryIds).to.not.be.eql([]) @@ -251,6 +254,7 @@ describe("query builder > relation-id > many-to-many > multiple-pk", () => { code: 1, }), ) + .addOrderBy("post.id") .getMany() expect(loadedPosts[0].categoryIds).to.not.be.eql([]) @@ -279,6 +283,7 @@ describe("query builder > relation-id > many-to-many > multiple-pk", () => { isRemoved: true, }), ) + .addOrderBy("post.id") .getMany() expect(loadedPosts[0].categoryIds).to.not.be.eql([]) @@ -362,6 +367,7 @@ describe("query builder > relation-id > many-to-many > multiple-pk", () => { "post.categoryIds", "post.subcategories", ) + .addOrderBy("post.id") .getMany() expect(loadedPosts[0].categoryIds).to.not.be.eql([]) @@ -474,6 +480,7 @@ describe("query builder > relation-id > many-to-many > multiple-pk", () => { "category.imageIds", "category.images", ) + .addOrderBy("post.id, category.id") .getMany() expect(loadedPosts[0].categoryIds).to.not.be.eql([]) @@ -612,6 +619,7 @@ describe("query builder > relation-id > many-to-many > multiple-pk", () => { "category.postIds", "category.posts", ) + .addOrderBy("category.id") .getMany() expect(loadedCategories[0].postIds).to.not.be.eql([]) @@ -698,6 +706,7 @@ describe("query builder > relation-id > many-to-many > multiple-pk", () => { "image.categoryIds", "image.categories", ) + .addOrderBy("image.id") .getMany() expect(loadedImages[0].categoryIds).to.not.be.eql([]) @@ -795,6 +804,7 @@ describe("query builder > relation-id > many-to-many > multiple-pk", () => { { id: 1, authorId: 1 }, ), ) + .addOrderBy("category.id") .getMany() expect(loadedCategories[0].postIds).to.not.be.eql([]) @@ -815,6 +825,7 @@ describe("query builder > relation-id > many-to-many > multiple-pk", () => { authorId: 1, }), ) + .addOrderBy("category.id") .getMany() expect(loadedCategories[0].postIds).to.not.be.eql([]) @@ -843,6 +854,7 @@ describe("query builder > relation-id > many-to-many > multiple-pk", () => { isRemoved: true, }), ) + .addOrderBy("category.id") .getMany() expect(loadedCategories[0].postIds).to.not.be.eql([]) diff --git a/test/functional/query-builder/relation-id/many-to-one/basic-functionality/basic-functionality.ts b/test/functional/query-builder/relation-id/many-to-one/basic-functionality/basic-functionality.ts index b21743d1cc..15e7ae3a19 100644 --- a/test/functional/query-builder/relation-id/many-to-one/basic-functionality/basic-functionality.ts +++ b/test/functional/query-builder/relation-id/many-to-one/basic-functionality/basic-functionality.ts @@ -60,6 +60,7 @@ describe("query builder > relation-id > many-to-one > basic-functionality", () = "post.categoryName", "post.categoryByName", ) + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].categoryId).to.not.be.undefined diff --git a/test/functional/query-builder/relation-id/many-to-one/multiple-pk/multiple-pk.ts b/test/functional/query-builder/relation-id/many-to-one/multiple-pk/multiple-pk.ts index c13488434a..af5046b5d2 100644 --- a/test/functional/query-builder/relation-id/many-to-one/multiple-pk/multiple-pk.ts +++ b/test/functional/query-builder/relation-id/many-to-one/multiple-pk/multiple-pk.ts @@ -105,6 +105,7 @@ describe("query builder > relation-id > many-to-one > multiple-pk", () => { const loadedCategories = await connection.manager .createQueryBuilder(Category, "category") .loadRelationIdAndMap("category.imageId", "category.image") + .addOrderBy("category.id") .getMany() expect(loadedCategories[0].imageId).to.be.equal(1) diff --git a/test/functional/query-builder/relation-id/one-to-many/basic-functionality/basic-functionality.ts b/test/functional/query-builder/relation-id/one-to-many/basic-functionality/basic-functionality.ts index c1ce0baf06..956604d311 100644 --- a/test/functional/query-builder/relation-id/one-to-many/basic-functionality/basic-functionality.ts +++ b/test/functional/query-builder/relation-id/one-to-many/basic-functionality/basic-functionality.ts @@ -53,16 +53,17 @@ describe("query builder > relation-id > one-to-many > basic-functionality", () = const loadedPosts = await connection.manager .createQueryBuilder(Post, "post") .loadRelationIdAndMap("post.categoryIds", "post.categories") + .addOrderBy("post.id") .getMany() expect(loadedPosts[0].categoryIds).to.not.be.eql([]) expect(loadedPosts[0].categoryIds.length).to.be.equal(2) - expect(loadedPosts[0].categoryIds[0]).to.be.equal(1) - expect(loadedPosts[0].categoryIds[1]).to.be.equal(2) + expect(loadedPosts[0].categoryIds).to.contain(1) + expect(loadedPosts[0].categoryIds).to.contain(2) expect(loadedPosts[1].categoryIds).to.not.be.eql([]) expect(loadedPosts[1].categoryIds.length).to.be.equal(2) - expect(loadedPosts[1].categoryIds[0]).to.be.equal(3) - expect(loadedPosts[1].categoryIds[1]).to.be.equal(4) + expect(loadedPosts[1].categoryIds).to.contain(3) + expect(loadedPosts[1].categoryIds).to.contain(4) const loadedPost = await connection.manager .createQueryBuilder(Post, "post") @@ -119,6 +120,7 @@ describe("query builder > relation-id > one-to-many > basic-functionality", () = isRemoved: true, }), ) + .addOrderBy("post.id") .getMany() expect(loadedPosts[0].categoryIds).to.not.be.eql([]) @@ -208,38 +210,38 @@ describe("query builder > relation-id > one-to-many > basic-functionality", () = "category.imageIds", "category.images", ) - .orderBy("category.id") + .orderBy("post.id, category.id") .getMany() expect(loadedPosts[0].categoryIds).to.not.be.eql([]) expect(loadedPosts[0].categoryIds.length).to.be.equal(2) - expect(loadedPosts[0].categoryIds[0]).to.be.equal(1) - expect(loadedPosts[0].categoryIds[1]).to.be.equal(2) + expect(loadedPosts[0].categoryIds).to.contain(1) + expect(loadedPosts[0].categoryIds).to.contain(2) expect(loadedPosts[0].categories).to.not.be.eql([]) expect(loadedPosts[0].categories.length).to.be.equal(2) expect(loadedPosts[0].categories[0].imageIds).to.not.be.eql([]) expect( loadedPosts[0].categories[0].imageIds.length, ).to.be.equal(2) - expect(loadedPosts[0].categories[0].imageIds[0]).to.be.equal(1) - expect(loadedPosts[0].categories[0].imageIds[1]).to.be.equal(2) + expect(loadedPosts[0].categories[0].imageIds).to.contain(1) + expect(loadedPosts[0].categories[0].imageIds).to.contain(2) expect(loadedPosts[0].categories[1].imageIds).to.not.be.eql([]) expect( loadedPosts[0].categories[1].imageIds.length, ).to.be.equal(1) - expect(loadedPosts[0].categories[1].imageIds[0]).to.be.equal(3) + expect(loadedPosts[0].categories[1].imageIds).to.contain(3) expect(loadedPosts[1].categoryIds).to.not.be.eql([]) expect(loadedPosts[1].categoryIds.length).to.be.equal(2) - expect(loadedPosts[1].categoryIds[0]).to.be.equal(3) - expect(loadedPosts[1].categoryIds[1]).to.be.equal(4) + expect(loadedPosts[1].categoryIds).to.contain(3) + expect(loadedPosts[1].categoryIds).to.contain(4) expect(loadedPosts[1].categories).to.not.be.eql([]) expect(loadedPosts[1].categories.length).to.be.equal(2) expect(loadedPosts[1].categories[0].imageIds).to.not.be.eql([]) expect( loadedPosts[1].categories[0].imageIds.length, ).to.be.equal(2) - expect(loadedPosts[1].categories[0].imageIds[0]).to.be.equal(4) - expect(loadedPosts[1].categories[0].imageIds[1]).to.be.equal(5) + expect(loadedPosts[1].categories[0].imageIds).to.contain(4) + expect(loadedPosts[1].categories[0].imageIds).to.contain(5) expect(loadedPosts[1].categories[1].imageIds).to.be.eql([]) const loadedPost = await connection.manager diff --git a/test/functional/query-builder/relation-id/one-to-many/embedded-with-multiple-pk/embedded-with-multiple-pk.ts b/test/functional/query-builder/relation-id/one-to-many/embedded-with-multiple-pk/embedded-with-multiple-pk.ts index f7ea13299d..43ab117a60 100644 --- a/test/functional/query-builder/relation-id/one-to-many/embedded-with-multiple-pk/embedded-with-multiple-pk.ts +++ b/test/functional/query-builder/relation-id/one-to-many/embedded-with-multiple-pk/embedded-with-multiple-pk.ts @@ -104,6 +104,10 @@ describe("query builder > relation-id > one-to-many > embedded-with-multiple-pk" .orderBy("post.id") .getMany() + // sort arrays because some drivers returns arrays in wrong order, e.g. categoryIds: [2, 1] + loadedPosts[0].counters.categoryIds.sort((a, b) => a.id - b.id) + loadedPosts[1].counters.categoryIds.sort((a, b) => a.id - b.id) + expect( loadedPosts[0].should.be.eql({ id: 1, diff --git a/test/functional/query-builder/relation-id/one-to-many/embedded-with-multiple-pk/entity/Counters.ts b/test/functional/query-builder/relation-id/one-to-many/embedded-with-multiple-pk/entity/Counters.ts index 43d8926c1f..a7da9b92bc 100644 --- a/test/functional/query-builder/relation-id/one-to-many/embedded-with-multiple-pk/entity/Counters.ts +++ b/test/functional/query-builder/relation-id/one-to-many/embedded-with-multiple-pk/entity/Counters.ts @@ -23,5 +23,5 @@ export class Counters { @Column(() => Subcounters, { prefix: "sub" }) subcounters: Subcounters - categoryIds: number[] + categoryIds: any[] } diff --git a/test/functional/query-builder/relation-id/one-to-many/embedded/embedded.ts b/test/functional/query-builder/relation-id/one-to-many/embedded/embedded.ts index 37c0ae306a..708e6f87b6 100644 --- a/test/functional/query-builder/relation-id/one-to-many/embedded/embedded.ts +++ b/test/functional/query-builder/relation-id/one-to-many/embedded/embedded.ts @@ -93,6 +93,12 @@ describe("query builder > relation-id > one-to-many > embedded", () => { .orderBy("post.id") .getMany() + // sort arrays because some drivers returns arrays in wrong order, e.g. categoryIds: [2, 1] + loadedPosts[0].counters.categoryIds.sort() + loadedPosts[0].counters.subcounters.watchedUserIds.sort() + loadedPosts[1].counters.categoryIds.sort() + loadedPosts[1].counters.subcounters.watchedUserIds.sort() + expect( loadedPosts[0].should.be.eql({ title: "About BMW", diff --git a/test/functional/query-builder/relation-id/one-to-many/multiple-pk/entity/Post.ts b/test/functional/query-builder/relation-id/one-to-many/multiple-pk/entity/Post.ts index 5c151e5dfc..3f7a318e15 100644 --- a/test/functional/query-builder/relation-id/one-to-many/multiple-pk/entity/Post.ts +++ b/test/functional/query-builder/relation-id/one-to-many/multiple-pk/entity/Post.ts @@ -18,5 +18,5 @@ export class Post { @OneToMany((type) => Category, (category) => category.post) categories: Category[] - categoryIds: number[] + categoryIds: any[] } diff --git a/test/functional/query-builder/relation-id/one-to-many/multiple-pk/multiple-pk.ts b/test/functional/query-builder/relation-id/one-to-many/multiple-pk/multiple-pk.ts index 57d8765def..66229bd8da 100644 --- a/test/functional/query-builder/relation-id/one-to-many/multiple-pk/multiple-pk.ts +++ b/test/functional/query-builder/relation-id/one-to-many/multiple-pk/multiple-pk.ts @@ -65,8 +65,13 @@ describe("query builder > relation-id > one-to-many > multiple-pk", () => { const loadedPosts = await connection.manager .createQueryBuilder(Post, "post") .loadRelationIdAndMap("post.categoryIds", "post.categories") + .addOrderBy("post.id") .getMany() + // sort arrays because some drivers returns arrays in wrong order, e.g. categoryIds: [2, 1] + loadedPosts[0].categoryIds.sort((a, b) => a.id - b.id) + loadedPosts[1].categoryIds.sort((a, b) => a.id - b.id) + expect(loadedPosts[0].categoryIds).to.not.be.eql([]) expect(loadedPosts[0].categoryIds[0]).to.be.eql({ id: 1, @@ -138,8 +143,13 @@ describe("query builder > relation-id > one-to-many > multiple-pk", () => { "category.imageIds", "category.images", ) + .addOrderBy("category.id") .getMany() + // sort arrays because some drivers returns arrays in wrong order, e.g. categoryIds: [2, 1] + loadedCategories[0].imageIds.sort() + loadedCategories[1].imageIds.sort() + expect(loadedCategories[0].imageIds).to.not.be.eql([]) expect(loadedCategories[0].imageIds[0]).to.be.equal(1) expect(loadedCategories[0].imageIds[1]).to.be.equal(2) @@ -218,6 +228,7 @@ describe("query builder > relation-id > one-to-many > multiple-pk", () => { { id: 1, code: 1 }, ), ) + .addOrderBy("post.id") .getMany() expect(loadedPosts[0].categoryIds).to.not.be.eql([]) @@ -236,8 +247,13 @@ describe("query builder > relation-id > one-to-many > multiple-pk", () => { (qb) => qb.andWhere("category.code = :code", { code: 1 }), ) + .addOrderBy("post.id") .getMany() + // sort arrays because some drivers returns arrays in wrong order, e.g. categoryIds: [2, 1] + loadedPosts[0].categoryIds.sort((a, b) => a.id - b.id) + loadedPosts[1].categoryIds.sort((a, b) => a.id - b.id) + expect(loadedPosts[0].categoryIds).to.not.be.eql([]) expect(loadedPosts[0].categoryIds[0]).to.be.eql({ id: 1, @@ -264,8 +280,13 @@ describe("query builder > relation-id > one-to-many > multiple-pk", () => { isRemoved: true, }), ) + .addOrderBy("post.id") .getMany() + // sort arrays because some drivers returns arrays in wrong order, e.g. categoryIds: [2, 1] + loadedPosts[0].categoryIds.sort((a, b) => a.id - b.id) + loadedPosts[1].categoryIds.sort((a, b) => a.id - b.id) + expect(loadedPosts[0].categoryIds).to.not.be.eql([]) expect(loadedPosts[0].categoryIds[0]).to.be.eql({ id: 2, @@ -363,7 +384,7 @@ describe("query builder > relation-id > one-to-many > multiple-pk", () => { "category.imageIds", "category.images", ) - .orderBy("category.id") + .orderBy("post.id, category.id") .getMany() expect(loadedPosts[0].categoryIds).to.not.be.eql([]) @@ -377,8 +398,8 @@ describe("query builder > relation-id > one-to-many > multiple-pk", () => { expect( loadedPosts[0].categories[0].imageIds.length, ).to.be.equal(2) - expect(loadedPosts[0].categories[0].imageIds[0]).to.be.equal(1) - expect(loadedPosts[0].categories[0].imageIds[1]).to.be.equal(2) + expect(loadedPosts[0].categories[0].imageIds).to.contain(1) + expect(loadedPosts[0].categories[0].imageIds).to.contain(2) expect(loadedPosts[1].categoryIds).to.not.be.eql([]) expect(loadedPosts[1].categoryIds.length).to.be.equal(2) expect(loadedPosts[1].categoryIds[0]).to.be.eql({ @@ -395,8 +416,8 @@ describe("query builder > relation-id > one-to-many > multiple-pk", () => { expect( loadedPosts[1].categories[0].imageIds.length, ).to.be.equal(2) - expect(loadedPosts[1].categories[0].imageIds[0]).to.be.equal(3) - expect(loadedPosts[1].categories[0].imageIds[1]).to.be.equal(4) + expect(loadedPosts[1].categories[0].imageIds).to.contain(3) + expect(loadedPosts[1].categories[0].imageIds).to.contain(4) expect(loadedPosts[1].categories[1].imageIds).to.not.be.eql([]) expect( loadedPosts[1].categories[1].imageIds.length, diff --git a/test/functional/query-builder/relation-id/one-to-one/basic-functionality/basic-functionality.ts b/test/functional/query-builder/relation-id/one-to-one/basic-functionality/basic-functionality.ts index 704427ed50..6024705f8f 100644 --- a/test/functional/query-builder/relation-id/one-to-one/basic-functionality/basic-functionality.ts +++ b/test/functional/query-builder/relation-id/one-to-one/basic-functionality/basic-functionality.ts @@ -44,6 +44,7 @@ describe("query builder > relation-id > one-to-one > basic-functionality", () => let loadedPosts = await connection.manager .createQueryBuilder(Post, "post") .loadRelationIdAndMap("post.categoryId", "post.category") + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].categoryId).to.not.be.undefined @@ -86,6 +87,7 @@ describe("query builder > relation-id > one-to-one > basic-functionality", () => let loadedCategories = await connection.manager .createQueryBuilder(Category, "category") .loadRelationIdAndMap("category.postId", "category.post") + .addOrderBy("category.id") .getMany() expect(loadedCategories![0].postId).to.not.be.undefined @@ -96,6 +98,7 @@ describe("query builder > relation-id > one-to-one > basic-functionality", () => let loadedCategory = await connection.manager .createQueryBuilder(Category, "category") .loadRelationIdAndMap("category.postId", "category.post") + .where("category.id = 1") .getOne() expect(loadedCategory!.postId).to.not.be.undefined diff --git a/test/functional/query-builder/relation-id/one-to-one/multiple-pk/entity/Image.ts b/test/functional/query-builder/relation-id/one-to-one/multiple-pk/entity/Image.ts index b1fb1b4a2a..c17179eaaf 100644 --- a/test/functional/query-builder/relation-id/one-to-one/multiple-pk/entity/Image.ts +++ b/test/functional/query-builder/relation-id/one-to-one/multiple-pk/entity/Image.ts @@ -1,12 +1,12 @@ import { Entity } from "../../../../../../../src/decorator/entity/Entity" -import { PrimaryGeneratedColumn } from "../../../../../../../src/decorator/columns/PrimaryGeneratedColumn" import { Column } from "../../../../../../../src/decorator/columns/Column" import { OneToOne } from "../../../../../../../src/decorator/relations/OneToOne" import { Category } from "./Category" +import { PrimaryColumn } from "../../../../../../../src" @Entity() export class Image { - @PrimaryGeneratedColumn() + @PrimaryColumn() id: number @Column() diff --git a/test/functional/query-builder/relation-id/one-to-one/multiple-pk/multiple-pk.ts b/test/functional/query-builder/relation-id/one-to-one/multiple-pk/multiple-pk.ts index fe9e45a4d4..e8e0c457ef 100644 --- a/test/functional/query-builder/relation-id/one-to-one/multiple-pk/multiple-pk.ts +++ b/test/functional/query-builder/relation-id/one-to-one/multiple-pk/multiple-pk.ts @@ -57,6 +57,7 @@ describe("query builder > relation-id > one-to-one > multiple-pk", () => { "post.categoryId", "post.category", ) + .addOrderBy("post.id") .getMany() expect(loadedPosts[0].categoryId).to.be.eql({ @@ -86,10 +87,12 @@ describe("query builder > relation-id > one-to-one > multiple-pk", () => { Promise.all( connections.map(async (connection) => { const image1 = new Image() + image1.id = 1 image1.name = "Image #1" await connection.manager.save(image1) const image2 = new Image() + image2.id = 2 image2.name = "Image #2" await connection.manager.save(image2) @@ -113,6 +116,7 @@ describe("query builder > relation-id > one-to-one > multiple-pk", () => { "category.imageId", "category.image", ) + .addOrderBy("category.id") .getMany() expect(loadedCategories[0].imageId).to.be.equal(1) @@ -167,6 +171,7 @@ describe("query builder > relation-id > one-to-one > multiple-pk", () => { "post.categoryId", "post.subcategory", ) + .addOrderBy("post.id") .getMany() expect(loadedPosts[0].categoryId).to.be.eql({ @@ -196,10 +201,12 @@ describe("query builder > relation-id > one-to-one > multiple-pk", () => { Promise.all( connections.map(async (connection) => { const image1 = new Image() + image1.id = 1 image1.name = "Image #1" await connection.manager.save(image1) const image2 = new Image() + image2.id = 2 image2.name = "Image #2" await connection.manager.save(image2) @@ -242,6 +249,7 @@ describe("query builder > relation-id > one-to-one > multiple-pk", () => { "category.imageId", "category.image", ) + .addOrderBy("category.imageId") .getMany() expect(loadedPosts[0].categoryId).to.be.eql({ @@ -312,6 +320,7 @@ describe("query builder > relation-id > one-to-one > multiple-pk", () => { "category.postId", "category.post", ) + .addOrderBy("category.id") .getMany() expect(loadedCategories[0].postId).to.be.eql({ @@ -356,11 +365,13 @@ describe("query builder > relation-id > one-to-one > multiple-pk", () => { await connection.manager.save(category2) const image1 = new Image() + image1.id = 1 image1.name = "Image #1" image1.category = category1 await connection.manager.save(image1) const image2 = new Image() + image2.id = 2 image2.name = "Image #2" image2.category = category2 await connection.manager.save(image2) @@ -371,6 +382,7 @@ describe("query builder > relation-id > one-to-one > multiple-pk", () => { "image.categoryId", "image.category", ) + .addOrderBy("image.id") .getMany() expect(loadedImages[0].categoryId).to.be.eql({ @@ -428,11 +440,13 @@ describe("query builder > relation-id > one-to-one > multiple-pk", () => { await connection.manager.save(category2) const image1 = new Image() + image1.id = 1 image1.name = "Image #1" image1.category = category1 await connection.manager.save(image1) const image2 = new Image() + image2.id = 2 image2.name = "Image #2" image2.category = category2 await connection.manager.save(image2) diff --git a/test/functional/query-runner/add-column.ts b/test/functional/query-runner/add-column.ts index 13c2940177..06f740c1fc 100644 --- a/test/functional/query-runner/add-column.ts +++ b/test/functional/query-runner/add-column.ts @@ -6,8 +6,8 @@ import { closeTestingConnections, createTestingConnections, } from "../../utils/test-utils" -import { PostgresDriver } from "../../../src/driver/postgres/PostgresDriver" import { DriverUtils } from "../../../src/driver/DriverUtils" +import { PostgresDriver } from "../../../src/driver/postgres/PostgresDriver" describe("query runner > add column", () => { let connections: DataSource[] @@ -23,25 +23,46 @@ describe("query runner > add column", () => { it("should correctly add column and revert add", () => Promise.all( connections.map(async (connection) => { + let numericType = "int" + if (DriverUtils.isSQLiteFamily(connection.driver)) { + numericType = "integer" + } else if (connection.driver.options.type === "spanner") { + numericType = "int64" + } + + let stringType = "varchar" + if (connection.driver.options.type === "spanner") { + stringType = "string" + } + const queryRunner = connection.createQueryRunner() let table = await queryRunner.getTable("post") let column1 = new TableColumn({ name: "secondId", - type: "int", + type: numericType, isUnique: true, - isNullable: false, + isNullable: connection.driver.options.type === "spanner", }) - // CockroachDB does not support altering primary key constraint - if (!(connection.driver.options.type === "cockroachdb")) + // CockroachDB and Spanner does not support altering primary key constraint + if ( + !( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + ) column1.isPrimary = true - // MySql and Sqlite does not supports autoincrement composite primary keys. + // MySql, CockroachDB and Sqlite does not supports autoincrement composite primary keys. + // Spanner does not support autoincrement. if ( - !DriverUtils.isMySQLFamily(connection.driver) && - !DriverUtils.isSQLiteFamily(connection.driver) && - !(connection.driver.options.type === "cockroachdb") + !( + DriverUtils.isMySQLFamily(connection.driver) || + DriverUtils.isSQLiteFamily(connection.driver) || + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) ) { column1.isGenerated = true column1.generationStrategy = "increment" @@ -49,17 +70,19 @@ describe("query runner > add column", () => { let column2 = new TableColumn({ name: "description", - type: "varchar", + type: stringType, length: "100", default: "'this is description'", + isNullable: connection.driver.options.type === "spanner", }) let column3 = new TableColumn({ name: "textAndTag", - type: "varchar", + type: stringType, length: "200", generatedType: "STORED", asExpression: "text || tag", + isNullable: connection.driver.options.type === "spanner", }) let column4 = new TableColumn({ @@ -68,6 +91,7 @@ describe("query runner > add column", () => { length: "200", generatedType: "VIRTUAL", asExpression: "text || tag", + isNullable: connection.driver.options.type === "spanner", }) await queryRunner.addColumn(table!, column1) @@ -77,17 +101,30 @@ describe("query runner > add column", () => { column1 = table!.findColumnByName("secondId")! column1!.should.be.exist column1!.isUnique.should.be.true - column1!.isNullable.should.be.false + if (connection.driver.options.type === "spanner") { + column1!.isNullable.should.be.true + } else { + column1!.isNullable.should.be.false + } - // CockroachDB does not support altering primary key constraint - if (!(connection.driver.options.type === "cockroachdb")) + // CockroachDB and Spanner does not support altering primary key constraint + if ( + !( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + ) column1!.isPrimary.should.be.true - // MySql and Sqlite does not supports autoincrement composite primary keys. + // MySql, CockroachDB and Sqlite does not supports autoincrement composite primary keys. + // Spanner does not support autoincrement. if ( - !DriverUtils.isMySQLFamily(connection.driver) && - !DriverUtils.isSQLiteFamily(connection.driver) && - !(connection.driver.options.type === "cockroachdb") + !( + DriverUtils.isMySQLFamily(connection.driver) || + DriverUtils.isSQLiteFamily(connection.driver) || + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) ) { column1!.isGenerated.should.be.true column1!.generationStrategy!.should.be.equal("increment") @@ -96,13 +133,20 @@ describe("query runner > add column", () => { column2 = table!.findColumnByName("description")! column2.should.be.exist column2.length.should.be.equal("100") - column2!.default!.should.be.equal("'this is description'") + + // Spanner does not support DEFAULT + if (!(connection.driver.options.type === "spanner")) { + column2!.default!.should.be.equal("'this is description'") + } if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "postgres" + connection.driver.options.type === "postgres" || + connection.driver.options.type === "spanner" ) { const isMySQL = connection.options.type === "mysql" + const isSpanner = + connection.driver.options.type === "spanner" let postgresSupported = false if (connection.driver.options.type === "postgres") { @@ -111,7 +155,7 @@ describe("query runner > add column", () => { ).isGeneratedColumnsSupported } - if (isMySQL || postgresSupported) { + if (isMySQL || isSpanner || postgresSupported) { await queryRunner.addColumn(table!, column3) table = await queryRunner.getTable("post") column3 = table!.findColumnByName("textAndTag")! diff --git a/test/functional/query-runner/change-column.ts b/test/functional/query-runner/change-column.ts index 655671c477..683cc14d72 100644 --- a/test/functional/query-runner/change-column.ts +++ b/test/functional/query-runner/change-column.ts @@ -23,15 +23,20 @@ describe("query runner > change column", () => { it("should correctly change column and revert change", () => Promise.all( connections.map(async (connection) => { - // CockroachDB does not allow changing primary columns and renaming constraints - if (connection.driver.options.type === "cockroachdb") return + // CockroachDB and Spanner does not allow changing primary columns and renaming constraints + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + return const queryRunner = connection.createQueryRunner() let table = await queryRunner.getTable("post") const nameColumn = table!.findColumnByName("name")! - nameColumn!.default!.should.exist + nameColumn!.isUnique.should.be.false + nameColumn!.default!.should.exist const changedNameColumn = nameColumn.clone() changedNameColumn.default = undefined @@ -100,8 +105,12 @@ describe("query runner > change column", () => { it("should correctly change column 'isGenerated' property and revert change", () => Promise.all( connections.map(async (connection) => { - // CockroachDB does not allow changing generated columns in existent tables - if (connection.driver.options.type === "cockroachdb") return + // CockroachDB and Spanner does not allow changing generated columns in existent tables + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + return const queryRunner = connection.createQueryRunner() let table = await queryRunner.getTable("post") @@ -174,21 +183,20 @@ describe("query runner > change column", () => { it("should correctly change generated as expression", () => Promise.all( connections.map(async (connection) => { - // Only works on postgres - if (!(connection.driver.options.type === "postgres")) return + const isPostgres = connection.driver.options.type === "postgres" + const isSpanner = connection.driver.options.type === "spanner" + const shouldRun = + (isPostgres && + (connection.driver as PostgresDriver) + .isGeneratedColumnsSupported) || + isSpanner + if (!shouldRun) return const queryRunner = connection.createQueryRunner() - // Database is running < postgres 12 - if ( - !(connection.driver as PostgresDriver) - .isGeneratedColumnsSupported - ) - return - let generatedColumn = new TableColumn({ name: "generated", - type: "character varying", + type: isSpanner ? "string" : "varchar", generatedType: "STORED", asExpression: "text || tag", }) diff --git a/test/functional/query-runner/create-check-constraint.ts b/test/functional/query-runner/create-check-constraint.ts index 3e0b36913f..ce88953302 100644 --- a/test/functional/query-runner/create-check-constraint.ts +++ b/test/functional/query-runner/create-check-constraint.ts @@ -26,6 +26,18 @@ describe("query runner > create check constraint", () => { // Mysql does not support check constraints. if (DriverUtils.isMySQLFamily(connection.driver)) return + let numericType = "int" + if (DriverUtils.isSQLiteFamily(connection.driver)) { + numericType = "integer" + } else if (connection.driver.options.type === "spanner") { + numericType = "int64" + } + + let stringType = "varchar" + if (connection.driver.options.type === "spanner") { + stringType = "string" + } + const queryRunner = connection.createQueryRunner() await queryRunner.createTable( new Table({ @@ -33,20 +45,20 @@ describe("query runner > create check constraint", () => { columns: [ { name: "id", - type: "int", + type: numericType, isPrimary: true, }, { name: "name", - type: "varchar", + type: stringType, }, { name: "description", - type: "varchar", + type: stringType, }, { name: "version", - type: "int", + type: numericType, }, ], }), @@ -76,11 +88,9 @@ describe("query runner > create check constraint", () => { "version", )} > 0`, }) - await queryRunner.createCheckConstraints("question", [ - check1, - check2, - check3, - ]) + await queryRunner.createCheckConstraint("question", check1) + await queryRunner.createCheckConstraint("question", check2) + await queryRunner.createCheckConstraint("question", check3) let table = await queryRunner.getTable("question") table!.checks.length.should.be.equal(3) diff --git a/test/functional/query-runner/create-foreign-key.ts b/test/functional/query-runner/create-foreign-key.ts index 0893135b2f..004eb3113d 100644 --- a/test/functional/query-runner/create-foreign-key.ts +++ b/test/functional/query-runner/create-foreign-key.ts @@ -7,6 +7,7 @@ import { } from "../../utils/test-utils" import { Table } from "../../../src/schema-builder/table/Table" import { TableForeignKey } from "../../../src/schema-builder/table/TableForeignKey" +import { DriverUtils } from "../../../src/driver/DriverUtils" describe("query runner > create foreign key", () => { let connections: DataSource[] @@ -23,6 +24,18 @@ describe("query runner > create foreign key", () => { it("should correctly create foreign key and revert creation", () => Promise.all( connections.map(async (connection) => { + let numericType = "int" + if (DriverUtils.isSQLiteFamily(connection.driver)) { + numericType = "integer" + } else if (connection.driver.options.type === "spanner") { + numericType = "int64" + } + + let stringType = "varchar" + if (connection.driver.options.type === "spanner") { + stringType = "string" + } + const queryRunner = connection.createQueryRunner() await queryRunner.createTable( new Table({ @@ -30,12 +43,12 @@ describe("query runner > create foreign key", () => { columns: [ { name: "id", - type: "int", + type: numericType, isPrimary: true, }, { name: "name", - type: "varchar", + type: stringType, }, ], }), @@ -48,19 +61,19 @@ describe("query runner > create foreign key", () => { columns: [ { name: "id", - type: "int", + type: numericType, isPrimary: true, }, { name: "name", - type: "varchar", + type: stringType, }, { name: "questionId", isUnique: connection.driver.options.type === "cockroachdb", // CockroachDB requires UNIQUE constraints on referenced columns - type: "int", + type: numericType, }, ], }), diff --git a/test/functional/query-runner/create-index.ts b/test/functional/query-runner/create-index.ts index c59e55266a..7a4db32005 100644 --- a/test/functional/query-runner/create-index.ts +++ b/test/functional/query-runner/create-index.ts @@ -7,6 +7,7 @@ import { } from "../../utils/test-utils" import { Table } from "../../../src/schema-builder/table/Table" import { TableIndex } from "../../../src/schema-builder/table/TableIndex" +import { DriverUtils } from "../../../src/driver/DriverUtils" describe("query runner > create index", () => { let connections: DataSource[] @@ -23,6 +24,18 @@ describe("query runner > create index", () => { it("should correctly create index and revert creation", () => Promise.all( connections.map(async (connection) => { + let numericType = "int" + if (DriverUtils.isSQLiteFamily(connection.driver)) { + numericType = "integer" + } else if (connection.driver.options.type === "spanner") { + numericType = "int64" + } + + let stringType = "varchar" + if (connection.driver.options.type === "spanner") { + stringType = "string" + } + const queryRunner = connection.createQueryRunner() await queryRunner.createTable( new Table({ @@ -30,16 +43,16 @@ describe("query runner > create index", () => { columns: [ { name: "id", - type: "int", + type: numericType, isPrimary: true, }, { name: "name", - type: "varchar", + type: stringType, }, { name: "description", - type: "varchar", + type: stringType, }, ], }), diff --git a/test/functional/query-runner/create-primary-key.ts b/test/functional/query-runner/create-primary-key.ts index 6393f4b185..0f6a5e98a8 100644 --- a/test/functional/query-runner/create-primary-key.ts +++ b/test/functional/query-runner/create-primary-key.ts @@ -22,8 +22,12 @@ describe("query runner > create primary key", () => { it("should correctly create primary key and revert creation", () => Promise.all( connections.map(async (connection) => { - // CockroachDB does not allow altering primary key - if (connection.driver.options.type === "cockroachdb") return + // CockroachDB and Spanner does not allow altering primary key + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + return const queryRunner = connection.createQueryRunner() await queryRunner.createTable( diff --git a/test/functional/query-runner/create-table.ts b/test/functional/query-runner/create-table.ts index 5d41fdd744..26da739467 100644 --- a/test/functional/query-runner/create-table.ts +++ b/test/functional/query-runner/create-table.ts @@ -9,7 +9,7 @@ import { Table } from "../../../src/schema-builder/table/Table" import { TableOptions } from "../../../src/schema-builder/options/TableOptions" import { Post } from "./entity/Post" import { Photo } from "./entity/Photo" -import { Book, Book2 } from "./entity/Book" +import { Book2, Book } from "./entity/Book" import { DriverUtils } from "../../../src/driver/DriverUtils" describe("query runner > create table", () => { @@ -26,21 +26,36 @@ describe("query runner > create table", () => { Promise.all( connections.map(async (connection) => { const queryRunner = connection.createQueryRunner() + + let numericType = "int" + if (DriverUtils.isSQLiteFamily(connection.driver)) { + numericType = "integer" + } else if (connection.driver.options.type === "spanner") { + numericType = "int64" + } + const options: TableOptions = { name: "category", columns: [ { name: "id", - type: DriverUtils.isSQLiteFamily(connection.driver) - ? "integer" - : "int", + type: numericType, isPrimary: true, - isGenerated: true, - generationStrategy: "increment", + isGenerated: + connection.driver.options.type === "spanner" + ? false + : true, + generationStrategy: + connection.driver.options.type === "spanner" + ? undefined + : "increment", }, { name: "name", - type: "varchar", + type: + connection.driver.options.type === "spanner" + ? "string" + : "varchar", isUnique: true, isNullable: false, }, @@ -54,16 +69,27 @@ describe("query runner > create table", () => { const nameColumn = table!.findColumnByName("name") idColumn!.should.be.exist idColumn!.isPrimary.should.be.true - idColumn!.isGenerated.should.be.true - idColumn!.generationStrategy!.should.be.equal("increment") + if (connection.driver.options.type === "spanner") { + idColumn!.isGenerated.should.be.false + expect(idColumn!.generationStrategy).to.be.undefined + } else { + idColumn!.isGenerated.should.be.true + idColumn!.generationStrategy!.should.be.equal("increment") + } nameColumn!.should.be.exist nameColumn!.isUnique.should.be.true table!.should.exist + if ( - !DriverUtils.isMySQLFamily(connection.driver) && - !(connection.driver.options.type === "sap") - ) + !( + DriverUtils.isMySQLFamily(connection.driver) || + connection.driver.options.type === "aurora-mysql" || + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" + ) + ) { table!.uniques.length.should.be.equal(1) + } await queryRunner.executeMemoryDownSql() table = await queryRunner.getTable("category") @@ -85,10 +111,15 @@ describe("query runner > create table", () => { const idColumn = table!.findColumnByName("id") const versionColumn = table!.findColumnByName("version") const nameColumn = table!.findColumnByName("name") + table!.should.exist if ( - !DriverUtils.isMySQLFamily(connection.driver) && - !(connection.driver.options.type === "sap") + !( + DriverUtils.isMySQLFamily(connection.driver) || + connection.driver.options.type === "aurora-mysql" || + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" + ) ) { table!.uniques.length.should.be.equal(2) table!.checks.length.should.be.equal(1) @@ -96,7 +127,11 @@ describe("query runner > create table", () => { idColumn!.isPrimary.should.be.true versionColumn!.isUnique.should.be.true - nameColumn!.default!.should.be.exist + + // Spanner does not support DEFAULT values + if (!(connection.driver.options.type === "spanner")) { + nameColumn!.default!.should.be.exist + } await queryRunner.release() }), @@ -107,23 +142,35 @@ describe("query runner > create table", () => { connections.map(async (connection) => { const queryRunner = connection.createQueryRunner() + let numericType = "int" + if (DriverUtils.isSQLiteFamily(connection.driver)) { + numericType = "integer" + } else if (connection.driver.options.type === "spanner") { + numericType = "int64" + } + + let stringType = "varchar" + if (connection.driver.options.type === "spanner") { + stringType = "string" + } + await queryRunner.createTable( new Table({ name: "person", columns: [ { name: "id", - type: "int", + type: numericType, isPrimary: true, }, { name: "userId", - type: "int", + type: numericType, isPrimary: true, }, { name: "name", - type: "varchar", + type: stringType, }, ], }), @@ -135,29 +182,33 @@ describe("query runner > create table", () => { columns: [ { name: "id", - type: DriverUtils.isSQLiteFamily(connection.driver) - ? "integer" - : "int", + type: numericType, isPrimary: true, - isGenerated: true, - generationStrategy: "increment", + isGenerated: + connection.driver.options.type === "spanner" + ? false + : true, + generationStrategy: + connection.driver.options.type === "spanner" + ? undefined + : "increment", }, { name: "name", - type: "varchar", + type: stringType, }, { name: "text", - type: "varchar", + type: stringType, isNullable: false, }, { name: "authorId", - type: "int", + type: numericType, }, { name: "authorUserId", - type: "int", + type: numericType, }, ], indices: [ @@ -177,7 +228,9 @@ describe("query runner > create table", () => { if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "aurora-mysql" || + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { questionTableOptions.indices!.push({ columnNames: ["name", "text"], @@ -205,27 +258,31 @@ describe("query runner > create table", () => { columns: [ { name: "id", - type: DriverUtils.isSQLiteFamily(connection.driver) - ? "integer" - : "int", + type: numericType, isPrimary: true, - isGenerated: true, - generationStrategy: "increment", + isGenerated: + connection.driver.options.type === "spanner" + ? false + : true, + generationStrategy: + connection.driver.options.type === "spanner" + ? undefined + : "increment", }, { name: "name", - type: "varchar", + type: stringType, default: "'default category'", isUnique: true, isNullable: false, }, { name: "alternativeName", - type: "varchar", + type: stringType, }, { name: "questionId", - type: "int", + type: numericType, isUnique: true, }, ], @@ -240,7 +297,9 @@ describe("query runner > create table", () => { if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "aurora-mysql" || + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { categoryTableOptions.indices = [ { columnNames: ["name", "alternativeName"] }, @@ -253,9 +312,13 @@ describe("query runner > create table", () => { // When we mark column as unique, MySql create index for that column and we don't need to create index separately. if ( - !DriverUtils.isMySQLFamily(connection.driver) && - !(connection.driver.options.type === "oracle") && - !(connection.driver.options.type === "sap") + !( + DriverUtils.isMySQLFamily(connection.driver) || + connection.driver.options.type === "aurora-mysql" || + connection.driver.options.type === "oracle" || + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" + ) ) categoryTableOptions.indices = [ { columnNames: ["questionId"] }, @@ -276,18 +339,22 @@ describe("query runner > create table", () => { let questionTable = await queryRunner.getTable("question") const questionIdColumn = questionTable!.findColumnByName("id") questionIdColumn!.isPrimary.should.be.true - questionIdColumn!.isGenerated.should.be.true - questionIdColumn!.generationStrategy!.should.be.equal( - "increment", - ) + if (!(connection.driver.options.type === "spanner")) { + questionIdColumn!.isGenerated.should.be.true + questionIdColumn!.generationStrategy!.should.be.equal( + "increment", + ) + } questionTable!.should.exist if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "aurora-mysql" || + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { - // MySql and SAP HANA does not have unique constraints. - // all unique constraints is unique indexes. + // MySql, SAP HANA and Spanner does not have unique constraints. + // all unique constraints are unique indexes. questionTable!.uniques.length.should.be.equal(0) questionTable!.indices.length.should.be.equal(2) } else if (connection.driver.options.type === "cockroachdb") { @@ -325,18 +392,22 @@ describe("query runner > create table", () => { const categoryTableIdColumn = categoryTable!.findColumnByName("id") categoryTableIdColumn!.isPrimary.should.be.true - categoryTableIdColumn!.isGenerated.should.be.true - categoryTableIdColumn!.generationStrategy!.should.be.equal( - "increment", - ) + if (!(connection.driver.options.type === "spanner")) { + categoryTableIdColumn!.isGenerated.should.be.true + categoryTableIdColumn!.generationStrategy!.should.be.equal( + "increment", + ) + } categoryTable!.should.exist categoryTable!.foreignKeys.length.should.be.equal(1) if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "aurora-mysql" || + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { - // MySql and SAP HANA does not have unique constraints. All unique constraints is unique indexes. + // MySql, SAP HANA and Spanner does not have unique constraints. All unique constraints are unique indexes. categoryTable!.indices.length.should.be.equal(3) } else if (connection.driver.options.type === "oracle") { // Oracle does not allow to put index on primary or unique columns. @@ -379,7 +450,9 @@ describe("query runner > create table", () => { if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "aurora-mysql" || + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { table!.uniques.length.should.be.equal(0) table!.indices.length.should.be.equal(4) @@ -410,51 +483,49 @@ describe("query runner > create table", () => { it("should correctly create table with different `withoutRowid` definitions", () => Promise.all( connections.map(async (connection) => { - if (DriverUtils.isSQLiteFamily(connection.driver)) { - const queryRunner = connection.createQueryRunner() - - // the table 'book' must contain a 'rowid' column - const metadataBook = connection.getMetadata(Book) - const newTableBook = Table.create( - metadataBook, - connection.driver, - ) - await queryRunner.createTable(newTableBook) - const aBook = new Book() - aBook.ean = "asdf" - await connection.manager.save(aBook) + if (!DriverUtils.isSQLiteFamily(connection.driver)) return - const desc = await connection.manager.query( - "SELECT rowid FROM book WHERE ean = 'asdf'", - ) - expect(desc[0].rowid).equals(1) + const queryRunner = connection.createQueryRunner() - await queryRunner.dropTable("book") - const bookTableIsGone = await queryRunner.getTable("book") - expect(bookTableIsGone).to.be.undefined + // the table 'book' must contain a 'rowid' column + const metadataBook = connection.getMetadata(Book) + const newTableBook = Table.create( + metadataBook, + connection.driver, + ) + await queryRunner.createTable(newTableBook) + const aBook = new Book() + aBook.ean = "asdf" + await connection.manager.save(aBook) - // the table 'book2' must NOT contain a 'rowid' column - const metadataBook2 = connection.getMetadata(Book2) - const newTableBook2 = Table.create( - metadataBook2, - connection.driver, - ) - await queryRunner.createTable(newTableBook2) + const desc = await connection.manager.query( + "SELECT rowid FROM book WHERE ean = 'asdf'", + ) + expect(desc[0].rowid).equals(1) - try { - await connection.manager.query( - "SELECT rowid FROM book2", - ) - } catch (e) { - expect(e.message).contains("no such column: rowid") - } + await queryRunner.dropTable("book") + const bookTableIsGone = await queryRunner.getTable("book") + expect(bookTableIsGone).to.be.undefined - await queryRunner.dropTable("book2") - const book2TableIsGone = await queryRunner.getTable("book2") - expect(book2TableIsGone).to.be.undefined + // the table 'book2' must NOT contain a 'rowid' column + const metadataBook2 = connection.getMetadata(Book2) + const newTableBook2 = Table.create( + metadataBook2, + connection.driver, + ) + await queryRunner.createTable(newTableBook2) - await queryRunner.release() + try { + await connection.manager.query("SELECT rowid FROM book2") + } catch (e) { + expect(e.message).contains("no such column: rowid") } + + await queryRunner.dropTable("book2") + const book2TableIsGone = await queryRunner.getTable("book2") + expect(book2TableIsGone).to.be.undefined + + await queryRunner.release() }), )) }) diff --git a/test/functional/query-runner/drop-column.ts b/test/functional/query-runner/drop-column.ts index 9ff9456cc8..93b17dd154 100644 --- a/test/functional/query-runner/drop-column.ts +++ b/test/functional/query-runner/drop-column.ts @@ -1,10 +1,10 @@ import "reflect-metadata" import { expect } from "chai" -import { DataSource } from "../../../src/data-source/DataSource" import { closeTestingConnections, createTestingConnections, } from "../../utils/test-utils" +import { DataSource } from "../../../src" describe("query runner > drop column", () => { let connections: DataSource[] @@ -42,8 +42,11 @@ describe("query runner > drop column", () => { // In Sqlite 'dropColumns' method is more optimal than 'dropColumn', because it recreate table just once, // without all removed columns. In other drivers it's no difference between these methods, because 'dropColumns' // calls 'dropColumn' method for each removed column. - // CockroachDB does not support changing pk. - if (connection.driver.options.type === "cockroachdb") { + // CockroachDB and Spanner does not support changing pk. + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) { await queryRunner.dropColumns(table!, [ nameColumn, versionColumn, @@ -59,7 +62,12 @@ describe("query runner > drop column", () => { table = await queryRunner.getTable("post") expect(table!.findColumnByName("name")).to.be.undefined expect(table!.findColumnByName("version")).to.be.undefined - if (!(connection.driver.options.type === "cockroachdb")) + if ( + !( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + ) expect(table!.findColumnByName("id")).to.be.undefined await queryRunner.executeMemoryDownSql() @@ -100,7 +108,10 @@ describe("query runner > drop column", () => { // without all removed columns. In other drivers it's no difference between these methods, because 'dropColumns' // calls 'dropColumn' method for each removed column. // CockroachDB does not support changing pk. - if (connection.driver.options.type === "cockroachdb") { + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) { await queryRunner.dropColumns(table!, [ "name", "version", @@ -116,7 +127,12 @@ describe("query runner > drop column", () => { table = await queryRunner.getTable("post") expect(table!.findColumnByName("name")).to.be.undefined expect(table!.findColumnByName("version")).to.be.undefined - if (!(connection.driver.options.type === "cockroachdb")) + if ( + !( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + ) expect(table!.findColumnByName("id")).to.be.undefined await queryRunner.executeMemoryDownSql() diff --git a/test/functional/query-runner/drop-primary-key.ts b/test/functional/query-runner/drop-primary-key.ts index bf4175c6a0..f55c07449a 100644 --- a/test/functional/query-runner/drop-primary-key.ts +++ b/test/functional/query-runner/drop-primary-key.ts @@ -22,7 +22,11 @@ describe("query runner > drop primary key", () => { Promise.all( connections.map(async (connection) => { // CockroachDB does not allow dropping primary key - if (connection.driver.options.type === "cockroachdb") return + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + return const queryRunner = connection.createQueryRunner() diff --git a/test/functional/query-runner/entity/Book.ts b/test/functional/query-runner/entity/Book.ts index 9e90e5c656..8560ec91e0 100644 --- a/test/functional/query-runner/entity/Book.ts +++ b/test/functional/query-runner/entity/Book.ts @@ -1,5 +1,4 @@ -import { PrimaryColumn } from "../../../../src/decorator/columns/PrimaryColumn" -import { Entity } from "../../../../src/decorator/entity/Entity" +import { Entity, PrimaryColumn } from "../../../../src" @Entity() export class Book { diff --git a/test/functional/query-runner/entity/Faculty.ts b/test/functional/query-runner/entity/Faculty.ts index 9e8eda6e72..c0388cf9e4 100644 --- a/test/functional/query-runner/entity/Faculty.ts +++ b/test/functional/query-runner/entity/Faculty.ts @@ -1,6 +1,4 @@ -import { Entity } from "../../../../src/decorator/entity/Entity" -import { Column } from "../../../../src/decorator/columns/Column" -import { PrimaryGeneratedColumn } from "../../../../src/decorator/columns/PrimaryGeneratedColumn" +import { Column, Entity, PrimaryGeneratedColumn } from "../../../../src" @Entity() export class Faculty { diff --git a/test/functional/query-runner/entity/Photo.ts b/test/functional/query-runner/entity/Photo.ts index eea0204570..cb7491ca2d 100644 --- a/test/functional/query-runner/entity/Photo.ts +++ b/test/functional/query-runner/entity/Photo.ts @@ -1,8 +1,4 @@ -import { Entity } from "../../../../src/decorator/entity/Entity" -import { Column } from "../../../../src/decorator/columns/Column" -import { Unique } from "../../../../src/decorator/Unique" -import { PrimaryColumn } from "../../../../src/decorator/columns/PrimaryColumn" -import { Index } from "../../../../src/decorator/Index" +import { Column, Entity, PrimaryColumn, Unique, Index } from "../../../../src" @Entity() @Unique(["name"]) diff --git a/test/functional/query-runner/entity/Post.ts b/test/functional/query-runner/entity/Post.ts index 05d5be0bd7..2c9a585245 100644 --- a/test/functional/query-runner/entity/Post.ts +++ b/test/functional/query-runner/entity/Post.ts @@ -1,14 +1,17 @@ -import { Entity } from "../../../../src/decorator/entity/Entity" -import { Column } from "../../../../src/decorator/columns/Column" -import { Unique } from "../../../../src/decorator/Unique" -import { PrimaryColumn } from "../../../../src/decorator/columns/PrimaryColumn" -import { Check } from "../../../../src/decorator/Check" -import { Exclusion } from "../../../../src/decorator/Exclusion" +import { + Check, + Column, + Entity, + Exclusion, + PrimaryColumn, + Unique, +} from "../../../../src" @Entity() @Unique(["text", "tag"]) @Exclusion(`USING gist ("name" WITH =)`) -@Check(`"version" < 999`) +@Check(`"version" < 999`) // should be properly escaped for each driver. +// @Check(`\`version\` < 999`) // should be properly escaped for each driver. export class Post { @PrimaryColumn() id: number diff --git a/test/functional/query-runner/entity/Student.ts b/test/functional/query-runner/entity/Student.ts index 08276d3fac..0fc9f3505d 100644 --- a/test/functional/query-runner/entity/Student.ts +++ b/test/functional/query-runner/entity/Student.ts @@ -1,10 +1,12 @@ -import { Entity } from "../../../../src/decorator/entity/Entity" -import { Column } from "../../../../src/decorator/columns/Column" -import { PrimaryGeneratedColumn } from "../../../../src/decorator/columns/PrimaryGeneratedColumn" +import { + Column, + Entity, + PrimaryGeneratedColumn, + ManyToOne, + Index, +} from "../../../../src" import { Faculty } from "./Faculty" -import { ManyToOne } from "../../../../src/decorator/relations/ManyToOne" import { Teacher } from "./Teacher" -import { Index } from "../../../../src/decorator/Index" @Entity() @Index("student_name_index", ["name"]) diff --git a/test/functional/query-runner/entity/Teacher.ts b/test/functional/query-runner/entity/Teacher.ts index cc9f21639c..79a4c996e7 100644 --- a/test/functional/query-runner/entity/Teacher.ts +++ b/test/functional/query-runner/entity/Teacher.ts @@ -1,8 +1,10 @@ -import { Entity } from "../../../../src/decorator/entity/Entity" -import { Column } from "../../../../src/decorator/columns/Column" -import { PrimaryGeneratedColumn } from "../../../../src/decorator/columns/PrimaryGeneratedColumn" +import { + Column, + Entity, + OneToMany, + PrimaryGeneratedColumn, +} from "../../../../src" import { Student } from "./Student" -import { OneToMany } from "../../../../src/decorator/relations/OneToMany" @Entity() export class Teacher { diff --git a/test/functional/query-runner/rename-table.ts b/test/functional/query-runner/rename-table.ts index 6f29204640..f493db5533 100644 --- a/test/functional/query-runner/rename-table.ts +++ b/test/functional/query-runner/rename-table.ts @@ -23,15 +23,19 @@ describe("query runner > rename table", () => { it("should correctly rename table and revert rename", () => Promise.all( connections.map(async (connection) => { + // CockroachDB and Spanner does not support renaming constraints and removing PK. + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + return + + const queryRunner = connection.createQueryRunner() + const sequenceQuery = (name: string) => { return `SELECT COUNT(*) FROM information_schema.sequences WHERE sequence_schema = 'public' and sequence_name = '${name}'` } - // CockroachDB does not support renaming constraints and removing PK. - if (connection.driver.options.type === "cockroachdb") return - - const queryRunner = connection.createQueryRunner() - // check if sequence "faculty_id_seq" exist if (connection.driver.options.type === "postgres") { const facultySeq = await queryRunner.query( @@ -98,8 +102,12 @@ describe("query runner > rename table", () => { it("should correctly rename table with all constraints depend to that table and revert rename", () => Promise.all( connections.map(async (connection) => { - // CockroachDB does not support renaming constraints and removing PK. - if (connection.driver.options.type === "cockroachdb") return + // CockroachDB and Spanner does not support renaming constraints and removing PK. + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + return const queryRunner = connection.createQueryRunner() @@ -142,8 +150,12 @@ describe("query runner > rename table", () => { it("should correctly rename table with custom schema and database and all its dependencies and revert rename", () => Promise.all( connections.map(async (connection) => { - // CockroachDB does not support renaming constraints and removing PK. - if (connection.driver.options.type === "cockroachdb") return + // CockroachDB and Spanner does not support renaming constraints and removing PK. + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + return const queryRunner = connection.createQueryRunner() let table: Table | undefined diff --git a/test/functional/query-runner/stream.ts b/test/functional/query-runner/stream.ts index 6a54cdf19f..5fda9c6965 100644 --- a/test/functional/query-runner/stream.ts +++ b/test/functional/query-runner/stream.ts @@ -19,6 +19,7 @@ describe("query runner > stream", () => { "postgres", "mssql", "oracle", + "spanner", ], }) }) @@ -38,11 +39,13 @@ describe("query runner > stream", () => { const query = connection .createQueryBuilder(Book, "book") .select() + .orderBy("book.ean") .getQuery() const readStream = await queryRunner.stream(query) - await new Promise((ok) => readStream.once("readable", ok)) + if (!(connection.driver.options.type === "spanner")) + await new Promise((ok) => readStream.once("readable", ok)) const data: any[] = [] diff --git a/test/functional/relations/custom-referenced-column-name/custom-referenced-column-name.ts b/test/functional/relations/custom-referenced-column-name/custom-referenced-column-name.ts index 2e8ee46c75..7fdc98ac5a 100644 --- a/test/functional/relations/custom-referenced-column-name/custom-referenced-column-name.ts +++ b/test/functional/relations/custom-referenced-column-name/custom-referenced-column-name.ts @@ -45,6 +45,7 @@ describe("relations > custom-referenced-column-name", () => { const loadedPosts = await connection.manager .createQueryBuilder(Post, "post") + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].categoryName).to.not.be.undefined @@ -91,6 +92,7 @@ describe("relations > custom-referenced-column-name", () => { "post.categoryWithEmptyJoinCol", "categoryWithEmptyJoinCol", ) + .addOrderBy("post.id") .getMany() expect( @@ -138,6 +140,7 @@ describe("relations > custom-referenced-column-name", () => { const loadedPosts = await connection.manager .createQueryBuilder(Post, "post") + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].categoryId).to.be.equal(1) @@ -179,6 +182,7 @@ describe("relations > custom-referenced-column-name", () => { "post.categoryWithoutColName", "categoryWithoutColName", ) + .addOrderBy("post.id") .getMany() expect( @@ -228,6 +232,7 @@ describe("relations > custom-referenced-column-name", () => { "post.categoryWithoutRefColName2", "categoryWithoutRefColName2", ) + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].categoryWithoutRefColName2).to.not.be @@ -282,6 +287,7 @@ describe("relations > custom-referenced-column-name", () => { const loadedPosts = await connection.manager .createQueryBuilder(Post, "post") .leftJoinAndSelect("post.category", "category") + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].category).to.not.be.undefined @@ -325,6 +331,7 @@ describe("relations > custom-referenced-column-name", () => { const loadedPosts = await connection.manager .createQueryBuilder(Post, "post") + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].tagName).to.not.be.undefined @@ -369,6 +376,7 @@ describe("relations > custom-referenced-column-name", () => { "post.tagWithEmptyJoinCol", "tagWithEmptyJoinCol", ) + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].tagWithEmptyJoinCol.id).to.be.equal( @@ -414,6 +422,7 @@ describe("relations > custom-referenced-column-name", () => { const loadedPosts = await connection.manager .createQueryBuilder(Post, "post") + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].tagId).to.be.equal(1) @@ -455,6 +464,7 @@ describe("relations > custom-referenced-column-name", () => { "post.tagWithoutColName", "tagWithoutColName", ) + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].tagWithoutColName.id).to.be.equal(1) @@ -500,6 +510,7 @@ describe("relations > custom-referenced-column-name", () => { "post.tagWithoutRefColName2", "tagWithoutRefColName2", ) + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].tagWithoutRefColName2).to.not.be @@ -552,6 +563,7 @@ describe("relations > custom-referenced-column-name", () => { const loadedPosts = await connection.manager .createQueryBuilder(Post, "post") .leftJoinAndSelect("post.tag", "tag") + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].tag).to.not.be.undefined diff --git a/test/functional/relations/eager-relations/basic-eager-relations/basic-eager-relations.ts b/test/functional/relations/eager-relations/basic-eager-relations/basic-eager-relations.ts index 61ff21308e..eebbd14362 100644 --- a/test/functional/relations/eager-relations/basic-eager-relations/basic-eager-relations.ts +++ b/test/functional/relations/eager-relations/basic-eager-relations/basic-eager-relations.ts @@ -72,6 +72,11 @@ describe("relations > eager relations > basic", () => { id: 1, }, }) + + // sort arrays because some drivers returns arrays in wrong order, e.g. categoryIds: [2, 1] + loadedPost!.categories1.sort((a, b) => a.id - b.id) + loadedPost!.categories2.sort((a, b) => a.id - b.id) + loadedPost!.should.be.eql({ id: 1, title: "about eager relations", diff --git a/test/functional/relations/multiple-primary-keys/multiple-primary-keys-many-to-many/entity/Category.ts b/test/functional/relations/multiple-primary-keys/multiple-primary-keys-many-to-many/entity/Category.ts index 2e8fdc66f1..3341af1049 100644 --- a/test/functional/relations/multiple-primary-keys/multiple-primary-keys-many-to-many/entity/Category.ts +++ b/test/functional/relations/multiple-primary-keys/multiple-primary-keys-many-to-many/entity/Category.ts @@ -9,12 +9,12 @@ import { Unique } from "../../../../../../src" @Entity() @Unique(["code", "version", "description"]) export class Category { - @PrimaryColumn("varchar", { + @PrimaryColumn(String, { length: 31, }) name: string - @PrimaryColumn("varchar", { + @PrimaryColumn(String, { length: 31, }) type: string diff --git a/test/functional/relations/multiple-primary-keys/multiple-primary-keys-many-to-many/entity/Tag.ts b/test/functional/relations/multiple-primary-keys/multiple-primary-keys-many-to-many/entity/Tag.ts index 913c3ea809..f8c937c92a 100644 --- a/test/functional/relations/multiple-primary-keys/multiple-primary-keys-many-to-many/entity/Tag.ts +++ b/test/functional/relations/multiple-primary-keys/multiple-primary-keys-many-to-many/entity/Tag.ts @@ -10,12 +10,12 @@ export class Tag { @Column() code: number - @PrimaryColumn("varchar", { + @PrimaryColumn(String, { length: 31, }) title: string - @PrimaryColumn("varchar", { + @PrimaryColumn(String, { length: 31, }) description: string diff --git a/test/functional/relations/multiple-primary-keys/multiple-primary-keys-one-to-many/entity/Setting.ts b/test/functional/relations/multiple-primary-keys/multiple-primary-keys-one-to-many/entity/Setting.ts index 5b8a9b42e0..f26065c23e 100644 --- a/test/functional/relations/multiple-primary-keys/multiple-primary-keys-one-to-many/entity/Setting.ts +++ b/test/functional/relations/multiple-primary-keys/multiple-primary-keys-one-to-many/entity/Setting.ts @@ -9,7 +9,7 @@ import { User } from "./User" @Entity() export class Setting extends BaseEntity { - @PrimaryColumn("int") + @PrimaryColumn() assetId?: number @ManyToOne("User", "settings", { @@ -19,7 +19,7 @@ export class Setting extends BaseEntity { }) asset?: User - @PrimaryColumn("varchar") + @PrimaryColumn() name: string @Column({ nullable: true }) diff --git a/test/functional/repository/basic-methods/model-schema/QuestionSchema.ts b/test/functional/repository/basic-methods/model-schema/QuestionSchema.ts index d0be5e1e15..45731a1a93 100644 --- a/test/functional/repository/basic-methods/model-schema/QuestionSchema.ts +++ b/test/functional/repository/basic-methods/model-schema/QuestionSchema.ts @@ -5,12 +5,12 @@ export default { }, columns: { id: { - type: "int", + type: Number, primary: true, generated: true, }, title: { - type: "varchar", + type: String, nullable: false, }, }, diff --git a/test/functional/repository/basic-methods/model-schema/UserSchema.ts b/test/functional/repository/basic-methods/model-schema/UserSchema.ts new file mode 100644 index 0000000000..123e9622bc --- /dev/null +++ b/test/functional/repository/basic-methods/model-schema/UserSchema.ts @@ -0,0 +1,21 @@ +export default { + name: "User", + table: { + name: "user", + }, + columns: { + id: { + type: Number, + primary: true, + generated: true, + }, + firstName: { + type: String, + nullable: false, + }, + secondName: { + type: String, + nullable: false, + }, + }, +} diff --git a/test/functional/repository/basic-methods/repository-basic-methods.ts b/test/functional/repository/basic-methods/repository-basic-methods.ts index 55ca871ad7..a9fa27d5f1 100644 --- a/test/functional/repository/basic-methods/repository-basic-methods.ts +++ b/test/functional/repository/basic-methods/repository-basic-methods.ts @@ -11,6 +11,7 @@ import { Post } from "./entity/Post" import { QueryBuilder } from "../../../../src/query-builder/QueryBuilder" import { User } from "./model/User" import questionSchema from "./model-schema/QuestionSchema" +import userSchema from "./model-schema/UserSchema" import { Question } from "./model/Question" import { Blog } from "./entity/Blog" import { Category } from "./entity/Category" @@ -24,17 +25,7 @@ import { OneToOneRelationEntity } from "./entity/OneToOneRelation" import { UpsertOptions } from "../../../../src/repository/UpsertOptions" describe("repository > basic methods", () => { - let userSchema: any - try { - const resourceDir = - __dirname + - "/../../../../../../test/functional/repository/basic-methods/" - userSchema = require(resourceDir + "schema/user.json") - } catch (err) { - const resourceDir = __dirname + "/" - userSchema = require(resourceDir + "schema/user.json") - } - const UserEntity = new EntitySchema(userSchema) + const UserEntity = new EntitySchema(userSchema as any) const QuestionEntity = new EntitySchema(questionSchema as any) let connections: DataSource[] diff --git a/test/functional/repository/basic-methods/schema/user.json b/test/functional/repository/basic-methods/schema/user.json index 6be1ce2fa0..bdd23e8ef7 100644 --- a/test/functional/repository/basic-methods/schema/user.json +++ b/test/functional/repository/basic-methods/schema/user.json @@ -18,4 +18,4 @@ "nullable": false } } -} \ No newline at end of file +} diff --git a/test/functional/repository/clear/entity/Post.ts b/test/functional/repository/clear/entity/Post.ts index 28afa1144f..b268fcc58d 100644 --- a/test/functional/repository/clear/entity/Post.ts +++ b/test/functional/repository/clear/entity/Post.ts @@ -4,7 +4,7 @@ import { PrimaryColumn } from "../../../../../src/decorator/columns/PrimaryColum @Entity() export class Post { - @PrimaryColumn("int") + @PrimaryColumn() id: number @Column() diff --git a/test/functional/repository/decrement/entity/Post.ts b/test/functional/repository/decrement/entity/Post.ts index 57af64f4ee..4005154454 100644 --- a/test/functional/repository/decrement/entity/Post.ts +++ b/test/functional/repository/decrement/entity/Post.ts @@ -4,7 +4,7 @@ import { PrimaryColumn } from "../../../../../src/decorator/columns/PrimaryColum @Entity() export class Post { - @PrimaryColumn("int") + @PrimaryColumn() id: number @Column() diff --git a/test/functional/repository/find-methods/repostiory-find-methods.ts b/test/functional/repository/find-methods/repostiory-find-methods.ts index 5ec3632b07..f5963ad3ef 100644 --- a/test/functional/repository/find-methods/repostiory-find-methods.ts +++ b/test/functional/repository/find-methods/repostiory-find-methods.ts @@ -560,7 +560,10 @@ describe("repository > find methods", () => { loadIds, ))! - loadedUsers.map((user) => user.id).should.be.eql(loadIds) + loadedUsers + .sort((a, b) => a.id - b.id) + .map((user) => user.id) + .should.be.eql(loadIds) }), )) }) diff --git a/test/functional/repository/find-methods/schema/UserEntity.ts b/test/functional/repository/find-methods/schema/UserEntity.ts index a4a9f39c30..f3b76900dc 100644 --- a/test/functional/repository/find-methods/schema/UserEntity.ts +++ b/test/functional/repository/find-methods/schema/UserEntity.ts @@ -9,11 +9,11 @@ export const UserEntity = new EntitySchema({ primary: true, }, firstName: { - type: "varchar", + type: String, nullable: false, }, secondName: { - type: "varchar", + type: String, nullable: false, }, }, diff --git a/test/functional/repository/find-options-locking/find-options-locking.ts b/test/functional/repository/find-options-locking/find-options-locking.ts index 1148ab3ebc..b4a27d4198 100644 --- a/test/functional/repository/find-options-locking/find-options-locking.ts +++ b/test/functional/repository/find-options-locking/find-options-locking.ts @@ -36,7 +36,8 @@ describe("repository > find options > locking", () => { connections.map(async (connection) => { if ( DriverUtils.isSQLiteFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) return @@ -83,7 +84,8 @@ describe("repository > find options > locking", () => { connections.map(async (connection) => { if ( DriverUtils.isSQLiteFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) return @@ -122,7 +124,8 @@ describe("repository > find options > locking", () => { if ( DriverUtils.isSQLiteFamily(connection.driver) || connection.driver.options.type === "cockroachdb" || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) return @@ -224,7 +227,8 @@ describe("repository > find options > locking", () => { connections.map(async (connection) => { if ( DriverUtils.isSQLiteFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) return diff --git a/test/functional/repository/find-options-operators/repository-find-operators.ts b/test/functional/repository/find-options-operators/repository-find-operators.ts index 7a45152a61..7c4fbd1079 100644 --- a/test/functional/repository/find-options-operators/repository-find-operators.ts +++ b/test/functional/repository/find-options-operators/repository-find-operators.ts @@ -217,6 +217,8 @@ describe("repository > find options > operators", () => { .findBy({ likes: MoreThanOrEqual(12), }) + + loadedPosts.sort((a, b) => a.id - b.id) loadedPosts.should.be.eql([ { id: 1, likes: 12, title: "About #1" }, { id: 3, likes: 13, title: "About #3" }, @@ -759,6 +761,7 @@ describe("repository > find options > operators", () => { { value1: 2, value2: 3 }, ), }) + result1.sort((a, b) => a.id - b.id) result1.should.be.eql([ { id: 2, likes: 2, title: "About #2" }, { id: 3, likes: 3, title: "About #3" }, @@ -773,6 +776,7 @@ describe("repository > find options > operators", () => { { maxValue: 6 }, ), }) + result2.sort((a, b) => a.id - b.id) result2.should.be.eql([ { id: 1, likes: 1, title: "About #1" }, { id: 4, likes: 4, title: "About #4" }, @@ -792,6 +796,7 @@ describe("repository > find options > operators", () => { e: 1, }), }) + result3.sort((a, b) => a.id - b.id) result3.should.be.eql([ { id: 1, likes: 1, title: "About #1" }, { id: 5, likes: 5, title: "About #5" }, @@ -801,6 +806,7 @@ describe("repository > find options > operators", () => { const result4 = await connection.getRepository(Post).findBy({ likes: Raw((columnAlias) => `${columnAlias} IN (2, 6)`, {}), }) + result4.sort((a, b) => a.id - b.id) result4.should.be.eql([ { id: 2, likes: 2, title: "About #2" }, { id: 6, likes: 6, title: "About #6" }, @@ -813,6 +819,7 @@ describe("repository > find options > operators", () => { { value: 3 }, ), }) + result5.sort((a, b) => a.id - b.id) result5.should.be.eql([ { id: 2, likes: 2, title: "About #2" }, { id: 3, likes: 3, title: "About #3" }, @@ -826,6 +833,7 @@ describe("repository > find options > operators", () => { { values: [2, 3, 6] }, ), }) + result6.sort((a, b) => a.id - b.id) result6.should.be.eql([ { id: 2, likes: 2, title: "About #2" }, { id: 3, likes: 3, title: "About #3" }, @@ -878,6 +886,7 @@ describe("repository > find options > operators", () => { likes: 4, }, ]) + loadedPosts.sort((a, b) => a.id - b.id) loadedPosts.should.be.eql([ { id: 2, likes: 3, title: "About #2" }, { id: 3, likes: 4, title: "About #3" }, diff --git a/test/functional/repository/find-options/repository-find-options.ts b/test/functional/repository/find-options/repository-find-options.ts index d71a1b26a5..0e55b6a45b 100644 --- a/test/functional/repository/find-options/repository-find-options.ts +++ b/test/functional/repository/find-options/repository-find-options.ts @@ -222,6 +222,7 @@ describe("repository > find options", () => { name: "Cats", }, ], + order: { id: "ASC" }, }) expect(loadedCategories2).to.be.eql([ diff --git a/test/functional/repository/increment/entity/Post.ts b/test/functional/repository/increment/entity/Post.ts index 57af64f4ee..4005154454 100644 --- a/test/functional/repository/increment/entity/Post.ts +++ b/test/functional/repository/increment/entity/Post.ts @@ -4,7 +4,7 @@ import { PrimaryColumn } from "../../../../../src/decorator/columns/PrimaryColum @Entity() export class Post { - @PrimaryColumn("int") + @PrimaryColumn() id: number @Column() diff --git a/test/functional/schema-builder/add-column.ts b/test/functional/schema-builder/add-column.ts index caff5c10f1..587ae32466 100644 --- a/test/functional/schema-builder/add-column.ts +++ b/test/functional/schema-builder/add-column.ts @@ -7,6 +7,7 @@ import { createTestingConnections, } from "../../utils/test-utils" import { Post } from "./entity/Post" +import { DriverUtils } from "../../../src/driver/DriverUtils" describe("schema builder > add column", () => { let connections: DataSource[] @@ -24,6 +25,18 @@ describe("schema builder > add column", () => { connections.map(async (connection) => { const postMetadata = connection.getMetadata("post") + let numericType = "int" + if (DriverUtils.isSQLiteFamily(connection.driver)) { + numericType = "integer" + } else if (connection.driver.options.type === "spanner") { + numericType = "int64" + } + + let stringType = "varchar" + if (connection.driver.options.type === "spanner") { + stringType = "string" + } + const columnMetadata1 = new ColumnMetadata({ connection: connection, entityMetadata: postMetadata!, @@ -32,12 +45,12 @@ describe("schema builder > add column", () => { propertyName: "secondId", mode: "regular", options: { - type: "int", + type: numericType, name: "secondId", - primary: !( - connection.driver.options.type === "cockroachdb" - ), // CockroachDB does not allow changing pk - nullable: false, + nullable: + connection.driver.options.type === "spanner" + ? true + : false, }, }, }) @@ -51,9 +64,13 @@ describe("schema builder > add column", () => { propertyName: "description", mode: "regular", options: { - type: "varchar", + type: stringType, name: "description", length: 100, + nullable: + connection.driver.options.type === "spanner" + ? true + : false, }, }, }) @@ -67,9 +84,11 @@ describe("schema builder > add column", () => { const table = await queryRunner.getTable("post") const column1 = table!.findColumnByName("secondId")! column1.should.be.exist - column1.isNullable.should.be.false - if (!(connection.driver.options.type === "cockroachdb")) - column1.isPrimary.should.be.true + if (connection.driver.options.type === "spanner") { + column1.isNullable.should.be.true + } else { + column1.isNullable.should.be.false + } const column2 = table!.findColumnByName("description")! column2.should.be.exist diff --git a/test/functional/schema-builder/change-check-constraint.ts b/test/functional/schema-builder/change-check-constraint.ts index 3189234c45..f83617be12 100644 --- a/test/functional/schema-builder/change-check-constraint.ts +++ b/test/functional/schema-builder/change-check-constraint.ts @@ -33,7 +33,9 @@ describe("schema builder > change check constraint", () => { entityMetadata: teacherMetadata, args: { target: Teacher, - expression: `"name" <> 'asd'`, + expression: `${connection.driver.escape( + "name", + )} <> 'asd'`, }, }) checkMetadata.build(connection.namingStrategy) @@ -56,7 +58,9 @@ describe("schema builder > change check constraint", () => { if (DriverUtils.isMySQLFamily(connection.driver)) return const postMetadata = connection.getMetadata(Post) - postMetadata.checks[0].expression = `"likesCount" < 2000` + postMetadata.checks[0].expression = `${connection.driver.escape( + "likesCount", + )} < 2000` postMetadata.checks[0].build(connection.namingStrategy) await connection.synchronize() diff --git a/test/functional/schema-builder/change-column.ts b/test/functional/schema-builder/change-column.ts index dffe98de1d..e23d0c18e3 100644 --- a/test/functional/schema-builder/change-column.ts +++ b/test/functional/schema-builder/change-column.ts @@ -73,7 +73,8 @@ describe("schema builder > change column", () => { if ( DriverUtils.isMySQLFamily(connection.driver) || connection.driver.options.type === "aurora-mysql" || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { postTable!.indices.length.should.be.equal(2) } else { @@ -92,13 +93,19 @@ describe("schema builder > change column", () => { const postMetadata = connection.getMetadata(Post) const versionColumn = postMetadata.findColumnWithPropertyName("version")! - versionColumn.type = "int" + versionColumn.type = + connection.driver.options.type === "spanner" + ? "int64" + : "int" // in test we must manually change referenced column too, but in real sync, it changes automatically const postVersionMetadata = connection.getMetadata(PostVersion) const postVersionColumn = postVersionMetadata.findColumnWithPropertyName("post")! - postVersionColumn.type = "int" + postVersionColumn.type = + connection.driver.options.type === "spanner" + ? "int64" + : "int" await connection.synchronize() @@ -111,14 +118,22 @@ describe("schema builder > change column", () => { postVersionTable!.foreignKeys.length.should.be.equal(1) // revert changes - versionColumn.type = "varchar" - postVersionColumn.type = "varchar" + if (connection.driver.options.type === "spanner") { + versionColumn.type = "string" + postVersionColumn.type = "string" + } else { + versionColumn.type = "varchar" + postVersionColumn.type = "varchar" + } }), )) it("should correctly change column default value", () => Promise.all( connections.map(async (connection) => { + // Spanner does not support DEFAULT + if (connection.driver.options.type === "spanner") return + const postMetadata = connection.getMetadata(Post) const nameColumn = postMetadata.findColumnWithPropertyName("name")! @@ -142,7 +157,11 @@ describe("schema builder > change column", () => { Promise.all( connections.map(async (connection) => { // CockroachDB does not allow changing PK - if (connection.driver.options.type === "cockroachdb") return + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + return const postMetadata = connection.getMetadata(Post) const idColumn = postMetadata.findColumnWithPropertyName("id")! @@ -214,8 +233,12 @@ describe("schema builder > change column", () => { it("should correctly change non-generated column on to uuid-generated column", () => Promise.all( connections.map(async (connection) => { - // CockroachDB does not allow changing PK - if (connection.driver.options.type === "cockroachdb") return + // CockroachDB and Spanner does not allow changing PK + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + return const queryRunner = connection.createQueryRunner() @@ -276,8 +299,12 @@ describe("schema builder > change column", () => { it("should correctly change generated column generation strategy", () => Promise.all( connections.map(async (connection) => { - // CockroachDB does not allow changing PK - if (connection.driver.options.type === "cockroachdb") return + // CockroachDB and Spanner does not allow changing PK + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + return const teacherMetadata = connection.getMetadata("teacher") const studentMetadata = connection.getMetadata("student") diff --git a/test/functional/schema-builder/change-unique-constraint.ts b/test/functional/schema-builder/change-unique-constraint.ts index b4604df769..3fa406f44f 100644 --- a/test/functional/schema-builder/change-unique-constraint.ts +++ b/test/functional/schema-builder/change-unique-constraint.ts @@ -35,7 +35,8 @@ describe("schema builder > change unique constraint", () => { // Mysql and SAP stores unique constraints as unique indices. if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { uniqueIndexMetadata = new IndexMetadata({ entityMetadata: teacherMetadata, @@ -68,7 +69,8 @@ describe("schema builder > change unique constraint", () => { if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { table!.indices.length.should.be.equal(1) table!.indices[0].isUnique!.should.be.true @@ -101,7 +103,8 @@ describe("schema builder > change unique constraint", () => { // Mysql and SAP stores unique constraints as unique indices. if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { const uniqueIndexMetadata = postMetadata.indices.find( (i) => i.columns.length === 2 && i.isUnique === true, @@ -122,7 +125,8 @@ describe("schema builder > change unique constraint", () => { if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { const tableIndex = table!.indices.find( (index) => @@ -169,7 +173,8 @@ describe("schema builder > change unique constraint", () => { // Mysql and SAP stores unique constraints as unique indices. if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { const index = postMetadata!.indices.find( (i) => i.columns.length === 2 && i.isUnique === true, @@ -196,7 +201,8 @@ describe("schema builder > change unique constraint", () => { if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { table!.indices.length.should.be.equal(1) } else { diff --git a/test/functional/schema-builder/create-table.ts b/test/functional/schema-builder/create-table.ts index 2f7255f262..39fa3117dd 100644 --- a/test/functional/schema-builder/create-table.ts +++ b/test/functional/schema-builder/create-table.ts @@ -40,7 +40,8 @@ describe("schema builder > create table", () => { if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { postTable!.indices.length.should.be.equal(2) } else { @@ -50,7 +51,9 @@ describe("schema builder > create table", () => { idColumn!.isPrimary.should.be.true versionColumn!.isUnique.should.be.true - nameColumn!.default!.should.be.exist + if (connection.driver.options.type !== "spanner") { + nameColumn!.default!.should.be.exist + } teacherTable = await queryRunner.getTable("teacher") teacherTable!.should.exist diff --git a/test/functional/schema-builder/entity/Post.ts b/test/functional/schema-builder/entity/Post.ts index 74e2f54720..59f4e4a05e 100644 --- a/test/functional/schema-builder/entity/Post.ts +++ b/test/functional/schema-builder/entity/Post.ts @@ -9,6 +9,7 @@ import { Exclusion } from "../../../../src/decorator/Exclusion" @Unique(["text", "tag"]) @Exclusion(`USING gist ("text" WITH =)`) @Check(`"likesCount" < 1000`) +// @Check(`\`likesCount\` < 1000`) // should be properly escaped for each driver. export class Post { @PrimaryColumn() id: number @@ -16,7 +17,7 @@ export class Post { @Column({ unique: true }) version: string - @Column({ default: "My post" }) + @Column({ nullable: true, default: "My post" }) name: string @Column({ nullable: true }) diff --git a/test/functional/schema-builder/update-primary-keys.ts b/test/functional/schema-builder/update-primary-keys.ts index cf104d542b..8da67aeb89 100644 --- a/test/functional/schema-builder/update-primary-keys.ts +++ b/test/functional/schema-builder/update-primary-keys.ts @@ -22,8 +22,12 @@ describe("schema builder > update primary keys", () => { it("should correctly update composite primary keys", () => Promise.all( connections.map(async (connection) => { - // CockroachDB does not support changing primary key constraint - if (connection.driver.options.type === "cockroachdb") return + // CockroachDB and Spanner does not support changing primary key constraint + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + return const metadata = connection.getMetadata(Category) const nameColumn = metadata.findColumnWithPropertyName("name") @@ -46,8 +50,12 @@ describe("schema builder > update primary keys", () => { // Sqlite does not support AUTOINCREMENT on composite primary key if (DriverUtils.isSQLiteFamily(connection.driver)) return - // CockroachDB does not support changing primary key constraint - if (connection.driver.options.type === "cockroachdb") return + // CockroachDB and Spanner does not support changing primary key constraint + if ( + connection.driver.options.type === "cockroachdb" || + connection.driver.options.type === "spanner" + ) + return const metadata = connection.getMetadata(Question) const nameColumn = metadata.findColumnWithPropertyName("name") diff --git a/test/functional/table-inheritance/single-table/basic-functionality/entity/Person.ts b/test/functional/table-inheritance/single-table/basic-functionality/entity/Person.ts index 67f2e7c6fc..53218ccfdb 100644 --- a/test/functional/table-inheritance/single-table/basic-functionality/entity/Person.ts +++ b/test/functional/table-inheritance/single-table/basic-functionality/entity/Person.ts @@ -4,7 +4,7 @@ import { Entity } from "../../../../../../src/decorator/entity/Entity" import { PrimaryGeneratedColumn } from "../../../../../../src/decorator/columns/PrimaryGeneratedColumn" @Entity() -@TableInheritance({ column: { name: "type", type: "varchar" } }) +@TableInheritance({ column: { name: "type", type: String } }) export class Person { @PrimaryGeneratedColumn() id: number diff --git a/test/functional/table-inheritance/single-table/no-type-column/entity/Note.ts b/test/functional/table-inheritance/single-table/no-type-column/entity/Note.ts index d271e77c67..a6d903546a 100644 --- a/test/functional/table-inheritance/single-table/no-type-column/entity/Note.ts +++ b/test/functional/table-inheritance/single-table/no-type-column/entity/Note.ts @@ -2,7 +2,7 @@ import * as TypeOrm from "../../../../../../src" import { Person } from "./Person" @TypeOrm.Entity() -@TypeOrm.TableInheritance({ column: { type: "varchar", name: "type" } }) +@TypeOrm.TableInheritance({ column: { type: String, name: "type" } }) export class Note { @TypeOrm.PrimaryGeneratedColumn() public id: number diff --git a/test/functional/table-inheritance/single-table/non-virtual-discriminator-column/entity/Person.ts b/test/functional/table-inheritance/single-table/non-virtual-discriminator-column/entity/Person.ts index c904e0e7c4..be823798b1 100644 --- a/test/functional/table-inheritance/single-table/non-virtual-discriminator-column/entity/Person.ts +++ b/test/functional/table-inheritance/single-table/non-virtual-discriminator-column/entity/Person.ts @@ -4,7 +4,7 @@ import { Entity } from "../../../../../../src/decorator/entity/Entity" import { PrimaryGeneratedColumn } from "../../../../../../src/decorator/columns/PrimaryGeneratedColumn" @Entity() -@TableInheritance({ column: { name: "type", type: "varchar" } }) +@TableInheritance({ column: { name: "type", type: String } }) export class Person { @PrimaryGeneratedColumn() id: number diff --git a/test/functional/table-inheritance/single-table/non-virtual-discriminator-column/non-virtual-discriminator-column.ts b/test/functional/table-inheritance/single-table/non-virtual-discriminator-column/non-virtual-discriminator-column.ts index c9db0f96e5..29822a8623 100644 --- a/test/functional/table-inheritance/single-table/non-virtual-discriminator-column/non-virtual-discriminator-column.ts +++ b/test/functional/table-inheritance/single-table/non-virtual-discriminator-column/non-virtual-discriminator-column.ts @@ -53,6 +53,7 @@ describe("table-inheritance > single-table > non-virtual-discriminator-column", let persons = await connection.manager .createQueryBuilder(Person, "person") + .addOrderBy("person.id") .getMany() persons[0].id.should.be.equal(1) diff --git a/test/functional/table-inheritance/single-table/numeric-types/entity/Person.ts b/test/functional/table-inheritance/single-table/numeric-types/entity/Person.ts index d833a6d809..79657a40fc 100644 --- a/test/functional/table-inheritance/single-table/numeric-types/entity/Person.ts +++ b/test/functional/table-inheritance/single-table/numeric-types/entity/Person.ts @@ -4,7 +4,7 @@ import { Entity } from "../../../../../../src/decorator/entity/Entity" import { PrimaryGeneratedColumn } from "../../../../../../src/decorator/columns/PrimaryGeneratedColumn" @Entity() -@TableInheritance({ column: { name: "type", type: "int" } }) +@TableInheritance({ column: { name: "type", type: Number } }) export class Person { @PrimaryGeneratedColumn() id: number diff --git a/test/functional/table-inheritance/single-table/numeric-types/numeric-types.ts b/test/functional/table-inheritance/single-table/numeric-types/numeric-types.ts index 61202947c6..fbd06425fa 100644 --- a/test/functional/table-inheritance/single-table/numeric-types/numeric-types.ts +++ b/test/functional/table-inheritance/single-table/numeric-types/numeric-types.ts @@ -48,6 +48,7 @@ describe("table-inheritance > single-table > numeric types", () => { let persons = await connection.manager .createQueryBuilder(Person, "person") + .addOrderBy("person.id") .getMany() expect(persons[0].id).to.be.equal(1) diff --git a/test/functional/table-inheritance/single-table/relations/many-to-many/entity/Person.ts b/test/functional/table-inheritance/single-table/relations/many-to-many/entity/Person.ts index 009a6907c2..2986ca00e3 100644 --- a/test/functional/table-inheritance/single-table/relations/many-to-many/entity/Person.ts +++ b/test/functional/table-inheritance/single-table/relations/many-to-many/entity/Person.ts @@ -4,7 +4,7 @@ import { Entity } from "../../../../../../../src/decorator/entity/Entity" import { PrimaryGeneratedColumn } from "../../../../../../../src/decorator/columns/PrimaryGeneratedColumn" @Entity() -@TableInheritance({ column: { name: "type", type: "varchar" } }) +@TableInheritance({ column: { name: "type", type: String } }) export class Person { @PrimaryGeneratedColumn() id: number diff --git a/test/functional/table-inheritance/single-table/relations/one-to-many-casecade-save/entity/Staff.ts b/test/functional/table-inheritance/single-table/relations/one-to-many-casecade-save/entity/Staff.ts index 234396cf47..f85c67f5f8 100644 --- a/test/functional/table-inheritance/single-table/relations/one-to-many-casecade-save/entity/Staff.ts +++ b/test/functional/table-inheritance/single-table/relations/one-to-many-casecade-save/entity/Staff.ts @@ -8,7 +8,7 @@ import { import { Faculty } from "./Faculty" @Entity() -@TableInheritance({ column: { name: "type", type: "varchar" } }) +@TableInheritance({ column: { name: "type", type: String } }) export class Staff { @PrimaryGeneratedColumn() id: number diff --git a/test/functional/table-inheritance/single-table/relations/one-to-many/entity/Person.ts b/test/functional/table-inheritance/single-table/relations/one-to-many/entity/Person.ts index 009a6907c2..2986ca00e3 100644 --- a/test/functional/table-inheritance/single-table/relations/one-to-many/entity/Person.ts +++ b/test/functional/table-inheritance/single-table/relations/one-to-many/entity/Person.ts @@ -4,7 +4,7 @@ import { Entity } from "../../../../../../../src/decorator/entity/Entity" import { PrimaryGeneratedColumn } from "../../../../../../../src/decorator/columns/PrimaryGeneratedColumn" @Entity() -@TableInheritance({ column: { name: "type", type: "varchar" } }) +@TableInheritance({ column: { name: "type", type: String } }) export class Person { @PrimaryGeneratedColumn() id: number diff --git a/test/functional/transaction/nested-transaction/transaction-in-entity-manager.ts b/test/functional/transaction/nested-transaction/transaction-in-entity-manager.ts index 64caaf756a..32f99e31ff 100644 --- a/test/functional/transaction/nested-transaction/transaction-in-entity-manager.ts +++ b/test/functional/transaction/nested-transaction/transaction-in-entity-manager.ts @@ -28,6 +28,9 @@ describe("transaction > nested transaction", () => { shouldExist: boolean }[] = [] + // Spanner does not support nested transactions + if (connection.driver.options.type === "spanner") return + await connection.manager.transaction(async (em0) => { const post = new Post() post.title = "Post #1" diff --git a/test/functional/transaction/single-query-runner/single-query-runner.ts b/test/functional/transaction/single-query-runner/single-query-runner.ts index 58768d848d..8c4151022f 100644 --- a/test/functional/transaction/single-query-runner/single-query-runner.ts +++ b/test/functional/transaction/single-query-runner/single-query-runner.ts @@ -82,9 +82,19 @@ describe("transaction > single query runner", () => { title: "Hello World", }) expect(loadedPost4).to.be.eql({ id: 1, title: "Hello World" }) - await entityManager.query( - `DELETE FROM ${connection.driver.escape("post")}`, - ) + + // in Spanner DELETE must have a WHERE clause + if (connection.driver.options.type === "spanner") { + await entityManager.query( + `DELETE FROM ${connection.driver.escape( + "post", + )} WHERE true`, + ) + } else { + await entityManager.query( + `DELETE FROM ${connection.driver.escape("post")}`, + ) + } const loadedPost5 = await entityManager.findOneBy(Post, { title: "Hello World", }) diff --git a/test/functional/tree-tables/closure-table/closure-table.ts b/test/functional/tree-tables/closure-table/closure-table.ts index 7a92c4290e..03411bd08b 100644 --- a/test/functional/tree-tables/closure-table/closure-table.ts +++ b/test/functional/tree-tables/closure-table/closure-table.ts @@ -246,6 +246,13 @@ describe("tree tables > closure-table", () => { await categoryRepository.save(a1) const categoriesTree = await categoryRepository.findTrees() + + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -316,6 +323,17 @@ describe("tree tables > closure-table", () => { await categoryRepository.save(b1) const categoriesTree = await categoryRepository.findTrees() + + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree[1].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[1].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -390,6 +408,13 @@ describe("tree tables > closure-table", () => { await categoryRepository.save(a1) const categoriesTree = await categoryRepository.findTrees() + + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -422,6 +447,15 @@ describe("tree tables > closure-table", () => { const categoriesTreeWithEmptyOptions = await categoryRepository.findTrees({}) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithEmptyOptions[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.should.be.eql([ { id: a1.id, @@ -464,6 +498,12 @@ describe("tree tables > closure-table", () => { const categoriesTreeWithDepthOne = await categoryRepository.findTrees({ depth: 1 }) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithDepthOne[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithDepthOne.should.be.eql([ { id: a1.id, @@ -512,6 +552,13 @@ describe("tree tables > closure-table", () => { const categoriesTree = await categoryRepository.findDescendantsTree(a1) + + // using sort because some drivers returns arrays in wrong order + categoriesTree.childCategories.sort((a, b) => a.id - b.id) + categoriesTree.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql({ id: a1.id, name: "a1", @@ -569,6 +616,13 @@ describe("tree tables > closure-table", () => { const categoriesTree = await categoryRepository.findDescendantsTree(a1) + + // using sort because some drivers returns arrays in wrong order + categoriesTree.childCategories.sort((a, b) => a.id - b.id) + categoriesTree.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql({ id: a1.id, name: "a1", @@ -599,6 +653,15 @@ describe("tree tables > closure-table", () => { const categoriesTreeWithEmptyOptions = await categoryRepository.findDescendantsTree(a1, {}) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithEmptyOptions.childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.should.be.eql({ id: a1.id, name: "a1", @@ -641,6 +704,12 @@ describe("tree tables > closure-table", () => { await categoryRepository.findDescendantsTree(a1, { depth: 1, }) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithDepthOne.childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithDepthOne.should.be.eql({ id: a1.id, name: "a1", diff --git a/test/functional/tree-tables/materialized-path/materialized-path.ts b/test/functional/tree-tables/materialized-path/materialized-path.ts index 8cdf4d013d..8cea9e3052 100644 --- a/test/functional/tree-tables/materialized-path/materialized-path.ts +++ b/test/functional/tree-tables/materialized-path/materialized-path.ts @@ -222,6 +222,12 @@ describe("tree tables > materialized-path", () => { await categoryRepository.save(a1) const categoriesTree = await categoryRepository.findTrees() + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -292,6 +298,16 @@ describe("tree tables > materialized-path", () => { await categoryRepository.save(b1) const categoriesTree = await categoryRepository.findTrees() + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree[1].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[1].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -366,6 +382,11 @@ describe("tree tables > materialized-path", () => { await categoryRepository.save(a1) const categoriesTree = await categoryRepository.findTrees() + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) categoriesTree.should.be.eql([ { id: a1.id, @@ -398,6 +419,13 @@ describe("tree tables > materialized-path", () => { const categoriesTreeWithEmptyOptions = await categoryRepository.findTrees({}) + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithEmptyOptions[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) categoriesTreeWithEmptyOptions.should.be.eql([ { id: a1.id, @@ -440,6 +468,12 @@ describe("tree tables > materialized-path", () => { const categoriesTreeWithDepthOne = await categoryRepository.findTrees({ depth: 1 }) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithDepthOne[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithDepthOne.should.be.eql([ { id: a1.id, @@ -488,6 +522,13 @@ describe("tree tables > materialized-path", () => { const categoriesTree = await categoryRepository.findDescendantsTree(a1) + + // using sort because some drivers returns arrays in wrong order + categoriesTree.childCategories.sort((a, b) => a.id - b.id) + categoriesTree.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql({ id: a1.id, name: "a1", @@ -545,6 +586,13 @@ describe("tree tables > materialized-path", () => { const categoriesTree = await categoryRepository.findDescendantsTree(a1) + + // using sort because some drivers returns arrays in wrong order + categoriesTree.childCategories.sort((a, b) => a.id - b.id) + categoriesTree.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql({ id: a1.id, name: "a1", @@ -575,6 +623,15 @@ describe("tree tables > materialized-path", () => { const categoriesTreeWithEmptyOptions = await categoryRepository.findDescendantsTree(a1, {}) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithEmptyOptions.childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.should.be.eql({ id: a1.id, name: "a1", diff --git a/test/functional/tree-tables/nested-set/nested-set.ts b/test/functional/tree-tables/nested-set/nested-set.ts index 02c2cf89d8..fb498634ce 100644 --- a/test/functional/tree-tables/nested-set/nested-set.ts +++ b/test/functional/tree-tables/nested-set/nested-set.ts @@ -222,6 +222,12 @@ describe("tree tables > nested-set", () => { await categoryRepository.save(a1) const categoriesTree = await categoryRepository.findTrees() + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -280,6 +286,13 @@ describe("tree tables > nested-set", () => { await categoryRepository.save(a1) const categoriesTree = await categoryRepository.findTrees() + + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -312,6 +325,15 @@ describe("tree tables > nested-set", () => { const categoriesTreeWithEmptyOptions = await categoryRepository.findTrees({}) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithEmptyOptions[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.should.be.eql([ { id: a1.id, @@ -354,6 +376,12 @@ describe("tree tables > nested-set", () => { const categoriesTreeWithDepthOne = await categoryRepository.findTrees({ depth: 1 }) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithDepthOne[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithDepthOne.should.be.eql([ { id: a1.id, @@ -422,6 +450,13 @@ describe("tree tables > nested-set", () => { const categoriesTree = await categoryRepository.findDescendantsTree(a1) + + // using sort because some drivers returns arrays in wrong order + categoriesTree.childCategories.sort((a, b) => a.id - b.id) + categoriesTree.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql({ id: a1.id, name: "a1", @@ -479,7 +514,13 @@ describe("tree tables > nested-set", () => { const categoriesTree = await categoryRepository.findDescendantsTree(a1) - console.log(categoriesTree) + + // using sort because some drivers returns arrays in wrong order + categoriesTree.childCategories.sort((a, b) => a.id - b.id) + categoriesTree.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql({ id: a1.id, name: "a1", @@ -510,6 +551,15 @@ describe("tree tables > nested-set", () => { const categoriesTreeWithEmptyOptions = await categoryRepository.findDescendantsTree(a1, {}) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithEmptyOptions.childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.should.be.eql({ id: a1.id, name: "a1", @@ -552,6 +602,12 @@ describe("tree tables > nested-set", () => { await categoryRepository.findDescendantsTree(a1, { depth: 1, }) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithDepthOne.childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithDepthOne.should.be.eql({ id: a1.id, name: "a1", diff --git a/test/functional/uuid/spanner/entity/Post.ts b/test/functional/uuid/spanner/entity/Post.ts new file mode 100644 index 0000000000..2473754c75 --- /dev/null +++ b/test/functional/uuid/spanner/entity/Post.ts @@ -0,0 +1,14 @@ +import { PrimaryGeneratedColumn } from "../../../../../src/decorator/columns/PrimaryGeneratedColumn" +import { Entity } from "../../../../../src/decorator/entity/Entity" +import { Column } from "../../../../../src/decorator/columns/Column" +import { Generated } from "../../../../../src/decorator/Generated" + +@Entity() +export class Post { + @PrimaryGeneratedColumn() + id: number + + @Column() + @Generated("uuid") + uuid: string +} diff --git a/test/functional/uuid/spanner/entity/Question.ts b/test/functional/uuid/spanner/entity/Question.ts new file mode 100644 index 0000000000..21c6bdf2f0 --- /dev/null +++ b/test/functional/uuid/spanner/entity/Question.ts @@ -0,0 +1,24 @@ +import { Entity } from "../../../../../src/decorator/entity/Entity" +import { PrimaryGeneratedColumn } from "../../../../../src/decorator/columns/PrimaryGeneratedColumn" +import { Column } from "../../../../../src/decorator/columns/Column" +import { Generated } from "../../../../../src/decorator/Generated" + +@Entity() +export class Question { + @PrimaryGeneratedColumn("uuid") + id: string + + @Column() + @Generated("uuid") + uuid: string + + @Column() + uuid2: string + + @Column("string", { nullable: true }) + uuid3: string | null + + @Column("string", { nullable: true }) + @Generated("uuid") + uuid4: string | null +} diff --git a/test/functional/uuid/spanner/uuid-spanner.ts b/test/functional/uuid/spanner/uuid-spanner.ts new file mode 100644 index 0000000000..a3ce35f623 --- /dev/null +++ b/test/functional/uuid/spanner/uuid-spanner.ts @@ -0,0 +1,102 @@ +import "reflect-metadata" +import { expect } from "chai" +import { DataSource } from "../../../../src/data-source/DataSource" +import { + closeTestingConnections, + createTestingConnections, + reloadTestingDatabases, +} from "../../../utils/test-utils" +import { Post } from "./entity/Post" +import { Question } from "./entity/Question" + +describe("uuid-spanner", () => { + let connections: DataSource[] + before(async () => { + connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + enabledDrivers: ["spanner"], + }) + }) + beforeEach(() => reloadTestingDatabases(connections)) + after(() => closeTestingConnections(connections)) + + it("should persist uuid correctly when it is generated non primary column", () => + Promise.all( + connections.map(async (connection) => { + const postRepository = connection.getRepository(Post) + const questionRepository = connection.getRepository(Question) + const queryRunner = connection.createQueryRunner() + const postTable = await queryRunner.getTable("post") + const questionTable = await queryRunner.getTable("question") + await queryRunner.release() + + const post = new Post() + await postRepository.save(post) + const loadedPost = await postRepository.findOneBy({ id: 1 }) + expect(loadedPost!.uuid).to.be.exist + postTable! + .findColumnByName("uuid")! + .type.should.be.equal("string") + + const post2 = new Post() + post2.uuid = "fd357b8f-8838-42f6-b7a2-ae027444e895" + await postRepository.save(post2) + const loadedPost2 = await postRepository.findOneBy({ id: 2 }) + expect(loadedPost2!.uuid).to.equal( + "fd357b8f-8838-42f6-b7a2-ae027444e895", + ) + + const question = new Question() + question.uuid2 = "fd357b8f-8838-42f6-b7a2-ae027444e895" + const savedQuestion = await questionRepository.save(question) + const loadedQuestion = await questionRepository.findOne({ + where: { + id: savedQuestion.id, + }, + }) + expect(loadedQuestion!.id).to.be.exist + expect(loadedQuestion!.uuid).to.be.exist + expect(loadedQuestion!.uuid2).to.equal( + "fd357b8f-8838-42f6-b7a2-ae027444e895", + ) + expect(loadedQuestion!.uuid3).to.be.null + expect(loadedQuestion!.uuid4).to.be.exist + questionTable! + .findColumnByName("id")! + .type.should.be.equal("string") + questionTable! + .findColumnByName("uuid")! + .type.should.be.equal("string") + questionTable! + .findColumnByName("uuid2")! + .type.should.be.equal("string") + questionTable! + .findColumnByName("uuid3")! + .type.should.be.equal("string") + + const question2 = new Question() + question2.id = "1ecad7f6-23ee-453e-bb44-16eca26d5189" + question2.uuid = "35b44650-b2cd-44ec-aa54-137fbdf1c373" + question2.uuid2 = "fd357b8f-8838-42f6-b7a2-ae027444e895" + question2.uuid3 = null + question2.uuid4 = null + await questionRepository.save(question2) + const loadedQuestion2 = await questionRepository.findOne({ + where: { + id: "1ecad7f6-23ee-453e-bb44-16eca26d5189", + }, + }) + expect(loadedQuestion2!.id).to.equal( + "1ecad7f6-23ee-453e-bb44-16eca26d5189", + ) + expect(loadedQuestion2!.uuid).to.equal( + "35b44650-b2cd-44ec-aa54-137fbdf1c373", + ) + expect(loadedQuestion2!.uuid2).to.equal( + "fd357b8f-8838-42f6-b7a2-ae027444e895", + ) + expect(loadedQuestion2!.uuid3).to.be.null + expect(loadedQuestion2!.uuid4).to.be.null + }), + )) +}) diff --git a/test/functional/view-entity/general/view-entity-general.ts b/test/functional/view-entity/general/view-entity-general.ts index 5b417bb02a..f0e537bccc 100644 --- a/test/functional/view-entity/general/view-entity-general.ts +++ b/test/functional/view-entity/general/view-entity-general.ts @@ -12,7 +12,6 @@ import { Photo } from "./entity/Photo" import { PhotoAlbumCategory } from "./entity/PhotoAlbumCategory" import { Post } from "./entity/Post" import { PostCategory } from "./entity/PostCategory" -import { CockroachDriver } from "../../../../src/driver/cockroachdb/CockroachDriver" import { PhotoAlbum } from "./entity/PhotoAlbum" describe("view entity > general", () => { @@ -119,14 +118,14 @@ describe("view entity > general", () => { photoAlbumCategories[0].categoryName.should.be.equal("Cars") const photoId2 = - connection.driver instanceof CockroachDriver ? "2" : 2 + connection.driver.options.type === "cockroachdb" ? "2" : 2 photoAlbumCategories[1].id.should.be.equal(photoId2) photoAlbumCategories[1].name.should.be.equal("BMW E60") photoAlbumCategories[1].albumName.should.be.equal("BMW photos") photoAlbumCategories[1].categoryName.should.be.equal("Cars") const albumId = - connection.driver instanceof CockroachDriver ? "1" : 1 + connection.driver.options.type === "cockroachdb" ? "1" : 1 const photoAlbumCategory = await connection.manager.findOneBy( PhotoAlbumCategory, { id: 1 }, @@ -139,7 +138,7 @@ describe("view entity > general", () => { const photoAlbums = await connection.manager.find(PhotoAlbum) const photoId3 = - connection.driver instanceof CockroachDriver ? "3" : 3 + connection.driver.options.type === "cockroachdb" ? "3" : 3 photoAlbums[0].id.should.be.equal(photoId3) photoAlbums[0].name.should.be.equal("boeing737") photoAlbums[0].albumName.should.be.equal("BOEING PHOTOS") diff --git a/test/github-issues/1099/issue-1099.ts b/test/github-issues/1099/issue-1099.ts index b85e93c1df..bf02b1c008 100644 --- a/test/github-issues/1099/issue-1099.ts +++ b/test/github-issues/1099/issue-1099.ts @@ -40,7 +40,8 @@ describe("github issues > #1099 BUG - QueryBuilder MySQL skip sql is wrong", () if ( DriverUtils.isMySQLFamily(connection.driver) || connection.driver.options.type === "aurora-mysql" || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { await qb .getManyAndCount() diff --git a/test/github-issues/1123/entity/Author.ts b/test/github-issues/1123/entity/Author.ts index 65971dc4a7..52476e7d3c 100644 --- a/test/github-issues/1123/entity/Author.ts +++ b/test/github-issues/1123/entity/Author.ts @@ -21,7 +21,7 @@ export const AuthorSchema: EntitySchemaOptions = { }, name: { - type: "varchar", + type: String, }, }, diff --git a/test/github-issues/1123/entity/Post.ts b/test/github-issues/1123/entity/Post.ts index fec175ae2d..f9094e65d1 100644 --- a/test/github-issues/1123/entity/Post.ts +++ b/test/github-issues/1123/entity/Post.ts @@ -21,7 +21,7 @@ export const PostSchema: EntitySchemaOptions = { }, title: { - type: "varchar", + type: String, }, }, diff --git a/test/github-issues/1261/issue-1261.ts b/test/github-issues/1261/issue-1261.ts index 2e31af735a..59d84d1300 100644 --- a/test/github-issues/1261/issue-1261.ts +++ b/test/github-issues/1261/issue-1261.ts @@ -19,6 +19,9 @@ describe("github issues > #1261 onDelete property on foreign key is not modified it("should modify onDelete property on foreign key on sync", () => Promise.all( connections.map(async (connection) => { + // Spanner support only NO ACTION clause + if (connection.driver.options.type === "spanner") return + await connection.synchronize() const queryRunner = connection.createQueryRunner() diff --git a/test/github-issues/131/entity/Person.ts b/test/github-issues/131/entity/Person.ts index ab95008ff9..29fa910ea5 100644 --- a/test/github-issues/131/entity/Person.ts +++ b/test/github-issues/131/entity/Person.ts @@ -4,9 +4,9 @@ import { Entity } from "../../../../src/decorator/entity/Entity" import { PrimaryColumn } from "../../../../src/decorator/columns/PrimaryColumn" @Entity() -@TableInheritance({ column: { name: "type", type: "varchar" } }) +@TableInheritance({ column: { name: "type", type: String } }) export class Person { - @PrimaryColumn("int") + @PrimaryColumn() id: number @Column() diff --git a/test/github-issues/1623/issue-1623.ts b/test/github-issues/1623/issue-1623.ts index 321637632f..41cfa8e699 100644 --- a/test/github-issues/1623/issue-1623.ts +++ b/test/github-issues/1623/issue-1623.ts @@ -22,8 +22,10 @@ describe("github issues > #1623 NOT NULL constraint failed after a new column is it("should correctly add new column", () => Promise.all( connections.map(async (connection) => { - const userMetadata = connection.getMetadata(User) + // Spanner does not support adding new NOT NULL column to existing table + if (connection.driver.options.type === "spanner") return + const userMetadata = connection.getMetadata(User) const columnMetadata = new ColumnMetadata({ connection: connection, entityMetadata: userMetadata, diff --git a/test/github-issues/1680/issue-1680.ts b/test/github-issues/1680/issue-1680.ts index 08fafe145d..d639596030 100644 --- a/test/github-issues/1680/issue-1680.ts +++ b/test/github-issues/1680/issue-1680.ts @@ -65,16 +65,18 @@ describe("github issues > #1680 Delete & Update applies to all entities in table }) // All users should still exist except for User C - await connection.manager.find(User).should.eventually.eql([ - { - id: 1, - name: "User A", - }, - { - id: 2, - name: "User B Updated", - }, - ]) + await connection.manager + .find(User, { order: { id: "asc" } }) + .should.eventually.eql([ + { + id: 1, + name: "User A", + }, + { + id: 2, + name: "User B Updated", + }, + ]) }), )) }) diff --git a/test/github-issues/175/issue-175.ts b/test/github-issues/175/issue-175.ts index c20c5fb9d8..f398c7d569 100644 --- a/test/github-issues/175/issue-175.ts +++ b/test/github-issues/175/issue-175.ts @@ -42,6 +42,7 @@ describe("github issues > #175 ManyToMany relation doesn't put an empty array wh .where("post.title = :title", { title: "post with categories", }) + .addOrderBy("categories.id") .getOne() expect(loadedPost).not.to.be.null diff --git a/test/github-issues/1780/entity/User.ts b/test/github-issues/1780/entity/User.ts index d5726f356a..0d9034ce98 100644 --- a/test/github-issues/1780/entity/User.ts +++ b/test/github-issues/1780/entity/User.ts @@ -7,10 +7,10 @@ import { Index } from "../../../../src/decorator/Index" export class User { @PrimaryGeneratedColumn() id: number - @Column({ type: "varchar", length: 100 }) + @Column({ length: 100 }) first_name: string - @Column({ type: "varchar", length: 100 }) + @Column({ length: 100 }) last_name: string - @Column({ type: "varchar", length: 100 }) + @Column({ length: 100 }) is_updated: string } diff --git a/test/github-issues/2103/issue-2103.ts b/test/github-issues/2103/issue-2103.ts index 4dab5c50f1..7f6ff29e25 100644 --- a/test/github-issues/2103/issue-2103.ts +++ b/test/github-issues/2103/issue-2103.ts @@ -41,6 +41,7 @@ describe("github issues > #2103 query builder regression", () => { .createQueryBuilder("s") .whereInIds(ids) .andWhere("s.x = 1") + .addOrderBy("s.id") .getMany() entities @@ -78,6 +79,7 @@ describe("github issues > #2103 query builder regression", () => { }), ) .andWhere("s.x = 1") + .addOrderBy("s.id") .getMany() entities diff --git a/test/github-issues/2201/entity/ver1/context.ts b/test/github-issues/2201/entity/ver1/context.ts index ae412e0d9b..debe6b3e06 100644 --- a/test/github-issues/2201/entity/ver1/context.ts +++ b/test/github-issues/2201/entity/ver1/context.ts @@ -22,6 +22,6 @@ export class RecordContext extends BaseEntity { @JoinColumn({ name: "user_id" }) public readonly user: User - @Column("simple-json") - public readonly meta: any + @Column() + public readonly meta: string } diff --git a/test/github-issues/2201/entity/ver2/context.ts b/test/github-issues/2201/entity/ver2/context.ts index 0cfb4bb7d7..b80fe8ad82 100644 --- a/test/github-issues/2201/entity/ver2/context.ts +++ b/test/github-issues/2201/entity/ver2/context.ts @@ -19,6 +19,6 @@ export class RecordContext extends BaseEntity { @ManyToOne((type) => User, (user) => user.contexts) public readonly user: User - @Column("simple-json") - public readonly meta: any + @Column() + public readonly meta: string } diff --git a/test/github-issues/2201/issue-2201.ts b/test/github-issues/2201/issue-2201.ts index 1e743c1265..35f6843432 100644 --- a/test/github-issues/2201/issue-2201.ts +++ b/test/github-issues/2201/issue-2201.ts @@ -54,7 +54,7 @@ describe("github issues > #2201 - Create a select query when using a (custom) ju record, userId: user.id, recordId: record.id, - meta: { name: "meta name", description: "meta description" }, + meta: "meta", } as RecordContext) await context.save() diff --git a/test/github-issues/2298/issue-2298.ts b/test/github-issues/2298/issue-2298.ts index eb8768459e..008072c226 100644 --- a/test/github-issues/2298/issue-2298.ts +++ b/test/github-issues/2298/issue-2298.ts @@ -74,6 +74,11 @@ describe("github issues > #2298 - Repository filtering not considering related c product: true, }, }, + order: { + ticketItems: { + id: "asc", + }, + }, }) loadedTicket.should.be.eql([ diff --git a/test/github-issues/2364/entity/dummy.ts b/test/github-issues/2364/entity/dummy.ts index 4a94292176..ff1b35b0f7 100644 --- a/test/github-issues/2364/entity/dummy.ts +++ b/test/github-issues/2364/entity/dummy.ts @@ -3,7 +3,7 @@ import { Entity } from "../../../../src/decorator/entity/Entity" @Entity() export class Dummy { - @Column("integer", { + @Column({ generated: true, nullable: false, primary: true, diff --git a/test/github-issues/2364/entity/dummy2.ts b/test/github-issues/2364/entity/dummy2.ts index ac563d0756..da256f4039 100644 --- a/test/github-issues/2364/entity/dummy2.ts +++ b/test/github-issues/2364/entity/dummy2.ts @@ -3,7 +3,7 @@ import { Column, PrimaryColumn } from "../../../../src" @Entity() export class Dummy2 { - @PrimaryColumn("integer", { + @PrimaryColumn({ generated: true, nullable: false, primary: true, diff --git a/test/github-issues/2364/issue-2364.ts b/test/github-issues/2364/issue-2364.ts index 5da9e6daf5..e1d160f4b3 100644 --- a/test/github-issues/2364/issue-2364.ts +++ b/test/github-issues/2364/issue-2364.ts @@ -21,6 +21,9 @@ describe("github issues > #2364 should generate id value if @Column generated:tr await reloadTestingDatabases(connections) await Promise.all( connections.map(async (connection) => { + // Spanner does not support auto-increment columns + if (connection.driver.options.type === "spanner") return + const repository1 = connection.getRepository(Dummy) const repository2 = connection.getRepository(Dummy2) let dummyObj1 = new Dummy() diff --git a/test/github-issues/2376/issue-2376.ts b/test/github-issues/2376/issue-2376.ts index f45e6d304c..04df08a78f 100644 --- a/test/github-issues/2376/issue-2376.ts +++ b/test/github-issues/2376/issue-2376.ts @@ -35,7 +35,10 @@ describe("github issues > #2376 Naming single column unique constraint with deco (it) => it.name === "unique-email-nickname", ) - if (DriverUtils.isMySQLFamily(connection.driver)) { + if ( + DriverUtils.isMySQLFamily(connection.driver) || + connection.driver.options.type === "spanner" + ) { unique1 = table!.indices.find( (it) => it.name === "unique-email", ) diff --git a/test/github-issues/2464/issue-2464.ts b/test/github-issues/2464/issue-2464.ts index c797099ec3..75b149a5cb 100644 --- a/test/github-issues/2464/issue-2464.ts +++ b/test/github-issues/2464/issue-2464.ts @@ -42,8 +42,10 @@ describe("github issues > #2464 - ManyToMany onDelete option not working", () => it("should delete when onDelete is not set", () => Promise.all( connections.map(async (connection) => { - const repo = connection.getRepository(Foo) + // Spanner support only NO ACTION clause + if (connection.driver.options.type === "spanner") return + const repo = connection.getRepository(Foo) await repo.save({ id: 1, otherBars: [{ description: "test1" }], diff --git a/test/github-issues/2557/entity/dummy.ts b/test/github-issues/2557/entity/dummy.ts index 774702ccc4..d7949f5e83 100644 --- a/test/github-issues/2557/entity/dummy.ts +++ b/test/github-issues/2557/entity/dummy.ts @@ -8,6 +8,6 @@ export class Dummy { @PrimaryGeneratedColumn() id: number - @Column("int", { transformer }) + @Column({ type: Number, transformer }) num: WrappedNumber } diff --git a/test/github-issues/2800/entity/Vehicle.ts b/test/github-issues/2800/entity/Vehicle.ts index 869184627b..15f9966f3b 100644 --- a/test/github-issues/2800/entity/Vehicle.ts +++ b/test/github-issues/2800/entity/Vehicle.ts @@ -7,7 +7,7 @@ import { export abstract class Engine {} @Entity() -@TableInheritance({ column: { name: "type", type: "varchar" } }) +@TableInheritance({ column: { name: "type", type: String } }) export abstract class Vehicle { @PrimaryGeneratedColumn() public id?: number diff --git a/test/github-issues/2965/index.ts b/test/github-issues/2965/index.ts index 45e638d940..e2e7aea3ab 100644 --- a/test/github-issues/2965/index.ts +++ b/test/github-issues/2965/index.ts @@ -52,7 +52,7 @@ describe("github issues > #2965 Reuse preloaded lazy relations", () => { loadCalledCounter.should.be.equal(0) personANotes[0].label.should.be.equal("note1") - const res2 = await repoPerson.find() + const res2 = await repoPerson.find({ order: { id: "asc" } }) const personBNotes = await res2[1].notes loadCalledCounter.should.be.equal(1) personBNotes[0].label.should.be.equal("note2") diff --git a/test/github-issues/2984/entity/issue/note.ts b/test/github-issues/2984/entity/issue/note.ts index 3355a169b8..682b676d4c 100755 --- a/test/github-issues/2984/entity/issue/note.ts +++ b/test/github-issues/2984/entity/issue/note.ts @@ -5,7 +5,7 @@ import { } from "../../../../../src" @Entity({ name: "issueNote" }) -@TableInheritance({ column: { type: "varchar", name: "type" } }) +@TableInheritance({ column: { type: String, name: "type" } }) export class Note { @PrimaryGeneratedColumn() public id: number diff --git a/test/github-issues/2984/entity/wiki/note.ts b/test/github-issues/2984/entity/wiki/note.ts index 4e83850715..96efe4e8d6 100755 --- a/test/github-issues/2984/entity/wiki/note.ts +++ b/test/github-issues/2984/entity/wiki/note.ts @@ -5,7 +5,7 @@ import { } from "../../../../../src" @Entity({ name: "wikiNote" }) -@TableInheritance({ column: { type: "varchar", name: "type" } }) +@TableInheritance({ column: { type: String, name: "type" } }) export class Note { @PrimaryGeneratedColumn() public id: number diff --git a/test/github-issues/3047/entity/User.ts b/test/github-issues/3047/entity/User.ts index d5726f356a..0d9034ce98 100644 --- a/test/github-issues/3047/entity/User.ts +++ b/test/github-issues/3047/entity/User.ts @@ -7,10 +7,10 @@ import { Index } from "../../../../src/decorator/Index" export class User { @PrimaryGeneratedColumn() id: number - @Column({ type: "varchar", length: 100 }) + @Column({ length: 100 }) first_name: string - @Column({ type: "varchar", length: 100 }) + @Column({ length: 100 }) last_name: string - @Column({ type: "varchar", length: 100 }) + @Column({ length: 100 }) is_updated: string } diff --git a/test/github-issues/3112/entity/User.ts b/test/github-issues/3112/entity/User.ts index 9b7742489b..9f38db4f10 100644 --- a/test/github-issues/3112/entity/User.ts +++ b/test/github-issues/3112/entity/User.ts @@ -6,11 +6,10 @@ export class User { @PrimaryGeneratedColumn() id: number - @Column({ type: "varchar", length: 100, nullable: true, default: null }) + @Column({ length: 100, nullable: true, default: null }) first: string @Column({ - type: "varchar", length: 100, nullable: true, default: () => "null", diff --git a/test/github-issues/3803/entity/Post.ts b/test/github-issues/3803/entity/Post.ts index 459ce30d4f..a67c4c7fe3 100644 --- a/test/github-issues/3803/entity/Post.ts +++ b/test/github-issues/3803/entity/Post.ts @@ -15,11 +15,11 @@ export const PostSchema: EntitySchemaOptions = { type: Number, }, name: { - type: "varchar", + type: String, unique: true, }, title: { - type: "varchar", + type: String, }, }, } diff --git a/test/github-issues/3803/issue-3803.ts b/test/github-issues/3803/issue-3803.ts index ba80d67b83..4460413229 100644 --- a/test/github-issues/3803/issue-3803.ts +++ b/test/github-issues/3803/issue-3803.ts @@ -30,7 +30,8 @@ describe("github issues > #3803 column option unique sqlite error", () => { // MySQL stores unique constraints as unique indices if ( DriverUtils.isMySQLFamily(connection.driver) || - connection.driver.options.type === "sap" + connection.driver.options.type === "sap" || + connection.driver.options.type === "spanner" ) { expect(table!.indices.length).to.be.equal(1) expect(table!.indices[0].isUnique).to.be.true diff --git a/test/github-issues/3946/issue-3946.ts b/test/github-issues/3946/issue-3946.ts index b391aab4f0..4e082aabb2 100644 --- a/test/github-issues/3946/issue-3946.ts +++ b/test/github-issues/3946/issue-3946.ts @@ -62,6 +62,7 @@ describe("github issues > #3946 loadRelationCountAndMap fails cause made a wrong "post.categoryCount", "post.categories", ) + .addOrderBy("post.id") .getMany() expect(loadedPosts![0].categoryCount).to.be.equal(3) @@ -417,6 +418,7 @@ describe("github issues > #3946 loadRelationCountAndMap fails cause made a wrong "category.postCount", "category.posts", ) + .addOrderBy("category.id") .getMany() expect(loadedCategories![0].postCount).to.be.equal(3) @@ -564,6 +566,7 @@ describe("github issues > #3946 loadRelationCountAndMap fails cause made a wrong isRemoved: true, }), ) + .addOrderBy("category.id") .getMany() expect(loadedCategories![0].postCount).to.be.equal(3) diff --git a/test/github-issues/3997/issue-3997.ts b/test/github-issues/3997/issue-3997.ts index 8268367fe4..8e98990651 100644 --- a/test/github-issues/3997/issue-3997.ts +++ b/test/github-issues/3997/issue-3997.ts @@ -12,6 +12,14 @@ describe("github issues > #3997 synchronize=true always failing when using decim before( async () => (connections = await createTestingConnections({ + enabledDrivers: [ + "postgres", + "oracle", + "cockroachdb", + "mssql", + "mysql", + "sqlite", + ], schemaCreate: false, dropSchema: true, entities: [User, Photo], diff --git a/test/github-issues/4277/entity/User.ts b/test/github-issues/4277/entity/User.ts index d772988d79..e6a39507c9 100644 --- a/test/github-issues/4277/entity/User.ts +++ b/test/github-issues/4277/entity/User.ts @@ -2,7 +2,7 @@ import { Entity, PrimaryGeneratedColumn, Column } from "../../../../src" @Entity() export class User { - @PrimaryGeneratedColumn({ type: "integer" }) + @PrimaryGeneratedColumn() id: number @Column() diff --git a/test/github-issues/4658/issue-4658.ts b/test/github-issues/4658/issue-4658.ts index 2197d0e30d..3cf6fb4d96 100644 --- a/test/github-issues/4658/issue-4658.ts +++ b/test/github-issues/4658/issue-4658.ts @@ -6,11 +6,12 @@ import { createTestingConnections, } from "../../utils/test-utils" -describe("query runner > rename column", () => { +describe("github issues > #4658 Renaming a column with current_timestamp(6) results in broken SQL", () => { let connections: DataSource[] before(async () => { connections = await createTestingConnections({ entities: [__dirname + "/entity/*{.js,.ts}"], + enabledDrivers: ["mysql", "mariadb"], schemaCreate: true, dropSchema: true, }) diff --git a/test/github-issues/495/entity/Item.ts b/test/github-issues/495/entity/Item.ts index 06cd856f62..ff4d373441 100644 --- a/test/github-issues/495/entity/Item.ts +++ b/test/github-issues/495/entity/Item.ts @@ -16,9 +16,9 @@ export class Item { @JoinColumn({ name: "userId" }) userData: User - @Column({ type: "int" }) + @Column() userId: number - @Column({ type: "int" }) + @Column() mid: number } diff --git a/test/github-issues/4980/issue-4980.ts b/test/github-issues/4980/issue-4980.ts index 938286a2bf..440aa17fd4 100644 --- a/test/github-issues/4980/issue-4980.ts +++ b/test/github-issues/4980/issue-4980.ts @@ -13,6 +13,7 @@ describe("github issues > #4980 (Postgres) onUpdate: 'CASCADE' doesn't work on m before( async () => (connections = await createTestingConnections({ + enabledDrivers: ["postgres"], entities: [Author, Book], })), ) diff --git a/test/github-issues/5501/issue-5501.ts b/test/github-issues/5501/issue-5501.ts index 4ef75f92d2..e5cb9194a6 100644 --- a/test/github-issues/5501/issue-5501.ts +++ b/test/github-issues/5501/issue-5501.ts @@ -12,6 +12,7 @@ describe("github issues > #5501 Incorrect data loading from JSON string for colu let connections: DataSource[] before(async () => { connections = await createTestingConnections({ + enabledDrivers: ["mysql", "mariadb"], entities: [Post], schemaCreate: true, dropSchema: true, diff --git a/test/github-issues/57/entity/AccessToken.ts b/test/github-issues/57/entity/AccessToken.ts index 4acf5cadb0..c3d38db19a 100644 --- a/test/github-issues/57/entity/AccessToken.ts +++ b/test/github-issues/57/entity/AccessToken.ts @@ -7,7 +7,7 @@ import { User } from "./User" @Entity() export class AccessToken { - @PrimaryColumn("int") + @PrimaryColumn() @Generated() primaryKey: number diff --git a/test/github-issues/57/entity/User.ts b/test/github-issues/57/entity/User.ts index c5a47802a6..540bc7a6a9 100644 --- a/test/github-issues/57/entity/User.ts +++ b/test/github-issues/57/entity/User.ts @@ -8,7 +8,7 @@ import { Generated } from "../../../../src/decorator/Generated" @Entity() export class User { - @PrimaryColumn("int") + @PrimaryColumn() @Generated() primaryKey: number diff --git a/test/github-issues/5762/entity/User.ts b/test/github-issues/5762/entity/User.ts index 7f90fbdff2..d83eb718fa 100644 --- a/test/github-issues/5762/entity/User.ts +++ b/test/github-issues/5762/entity/User.ts @@ -7,7 +7,8 @@ export class User { @PrimaryColumn() id: number - @Column("varchar", { + @Column({ + type: String, // marshall transformer: { from(value: string): URL { diff --git a/test/github-issues/58/issue-58.ts b/test/github-issues/58/issue-58.ts index fe2f681deb..4a18f47bb2 100644 --- a/test/github-issues/58/issue-58.ts +++ b/test/github-issues/58/issue-58.ts @@ -54,6 +54,7 @@ describe("github issues > #58 relations with multiple primary keys", () => { .createQueryBuilder(Post, "post") .innerJoinAndSelect("post.categories", "postCategory") .innerJoinAndSelect("postCategory.category", "category") + .addOrderBy("postCategory.categoryId") .getOne() expect(loadedPost!).not.to.be.null diff --git a/test/github-issues/6950/entity/post_with_null_2.entity.ts b/test/github-issues/6950/entity/post_with_null_2.entity.ts index 7742755219..085b392e1f 100644 --- a/test/github-issues/6950/entity/post_with_null_2.entity.ts +++ b/test/github-issues/6950/entity/post_with_null_2.entity.ts @@ -21,7 +21,7 @@ export class Post extends BaseEntity { @Column({ nullable: true, default: null, - type: "varchar", + type: String, }) comments: string | null } diff --git a/test/github-issues/70/issue-70.ts b/test/github-issues/70/issue-70.ts index 39dfe40582..99d62f2d47 100644 --- a/test/github-issues/70/issue-70.ts +++ b/test/github-issues/70/issue-70.ts @@ -23,6 +23,9 @@ describe("github issues > #70 cascade deleting works incorrect", () => { it("should persist successfully and return persisted entity", () => Promise.all( connections.map(async (connection) => { + // Spanner support only NO ACTION clause + if (connection.driver.options.type === "spanner") return + // create objects to save const category1 = new Category() category1.name = "category #1" diff --git a/test/github-issues/7065/entity/Contact.ts b/test/github-issues/7065/entity/Contact.ts index 7b140971c8..0ef1d5e231 100644 --- a/test/github-issues/7065/entity/Contact.ts +++ b/test/github-issues/7065/entity/Contact.ts @@ -6,7 +6,7 @@ import { } from "../../../../src" @Entity() -@TableInheritance({ column: { type: "varchar", name: "type" } }) +@TableInheritance({ column: { type: String, name: "type" } }) export class Contact { @PrimaryGeneratedColumn() id: number diff --git a/test/github-issues/71/entity/Artikel.ts b/test/github-issues/71/entity/Artikel.ts index 89c00c081e..b775eb30f6 100644 --- a/test/github-issues/71/entity/Artikel.ts +++ b/test/github-issues/71/entity/Artikel.ts @@ -8,7 +8,7 @@ import { Generated } from "../../../../src/decorator/Generated" @Entity("artikel") export class Artikel { - @PrimaryColumn("int", { name: "artikel_id" }) + @PrimaryColumn({ name: "artikel_id" }) @Generated() id: number diff --git a/test/github-issues/71/entity/Kollektion.ts b/test/github-issues/71/entity/Kollektion.ts index 4cfb5e7c35..ca0eee03cf 100644 --- a/test/github-issues/71/entity/Kollektion.ts +++ b/test/github-issues/71/entity/Kollektion.ts @@ -5,7 +5,7 @@ import { Generated } from "../../../../src/decorator/Generated" @Entity("kollektion") export class Kollektion { - @PrimaryColumn("int", { name: "kollektion_id" }) + @PrimaryColumn({ name: "kollektion_id" }) @Generated() id: number diff --git a/test/github-issues/7109/issue-7109.ts b/test/github-issues/7109/issue-7109.ts index cc27c36d11..083fb9c6a3 100644 --- a/test/github-issues/7109/issue-7109.ts +++ b/test/github-issues/7109/issue-7109.ts @@ -26,7 +26,13 @@ describe("github issues > #7109 stream() bug from 0.2.25 to 0.2.26 with postgres entities: [__dirname + "/entity/*{.js,.ts}"], schemaCreate: true, dropSchema: true, - enabledDrivers: ["postgres", "mysql", "mariadb", "cockroachdb"], + enabledDrivers: [ + "postgres", + "mysql", + "mariadb", + "cockroachdb", + "spanner", + ], })), ) beforeEach(() => reloadTestingDatabases(connections)) diff --git a/test/github-issues/7415/issue-7415.ts b/test/github-issues/7415/issue-7415.ts index f36fd0bc0d..0630922cce 100644 --- a/test/github-issues/7415/issue-7415.ts +++ b/test/github-issues/7415/issue-7415.ts @@ -67,7 +67,10 @@ describe("github issues > #7415 Tree entities with embedded primary columns are ], } - expect(descendantsTree).to.be.eql(expectedDescendantsTree) + expect(descendantsTree.id).to.be.eql(expectedDescendantsTree.id) + expect(descendantsTree.children).to.have.deep.members( + expectedDescendantsTree.children, + ) const ancestorsTree = await repository.findAncestorsTree(a121) diff --git a/test/github-issues/8076/issue-8076.ts b/test/github-issues/8076/issue-8076.ts index 838c8ce2f2..ffa328406a 100644 --- a/test/github-issues/8076/issue-8076.ts +++ b/test/github-issues/8076/issue-8076.ts @@ -173,6 +173,8 @@ describe("github issues > #8076 Add relation options to all tree queries (missin .getTreeRepository(Category) .findRoots({ relations: ["members"] }) + result.sort((a, b) => a.pk - b.pk) + expect(result).to.have.lengthOf(2) expect(result[0].sites).equals(undefined) expect(result[0].members).to.have.lengthOf(1) diff --git a/test/github-issues/8221/entity/Setting.ts b/test/github-issues/8221/entity/Setting.ts index dab902811d..3a2dc32df8 100644 --- a/test/github-issues/8221/entity/Setting.ts +++ b/test/github-issues/8221/entity/Setting.ts @@ -9,7 +9,7 @@ import { User } from "./User" @Entity() export class Setting extends BaseEntity { - @PrimaryColumn("int") + @PrimaryColumn() assetId?: number @ManyToOne("User", "settings", { @@ -19,7 +19,7 @@ export class Setting extends BaseEntity { }) asset?: User - @PrimaryColumn("varchar") + @PrimaryColumn() name: string @Column({ nullable: true }) diff --git a/test/github-issues/8221/issue-8221.ts b/test/github-issues/8221/issue-8221.ts index e6eeebd02e..f7313f00e5 100644 --- a/test/github-issues/8221/issue-8221.ts +++ b/test/github-issues/8221/issue-8221.ts @@ -44,7 +44,11 @@ describe("github issues > #8221", () => { it("afterLoad entity modifier must not make relation key matching fail", async () => { for (const connection of connections) { const userRepo = connection.getRepository(User) - const subscriber = connection.subscribers[0] as SettingSubscriber + const subscriber = connection.subscribers.find( + (s) => s instanceof SettingSubscriber, + ) as SettingSubscriber + if (!subscriber) throw new Error(`Subscriber not found`) + subscriber.reset() await insertSimpleTestData(connection) diff --git a/test/github-issues/8443/closure-table/closure-table.ts b/test/github-issues/8443/closure-table/closure-table.ts index 260d94eaa6..820ed16a76 100644 --- a/test/github-issues/8443/closure-table/closure-table.ts +++ b/test/github-issues/8443/closure-table/closure-table.ts @@ -246,6 +246,13 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum await categoryRepository.save(a1) const categoriesTree = await categoryRepository.findTrees() + + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -316,6 +323,17 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum await categoryRepository.save(b1) const categoriesTree = await categoryRepository.findTrees() + + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree[1].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[1].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -390,6 +408,13 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum await categoryRepository.save(a1) const categoriesTree = await categoryRepository.findTrees() + + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -422,6 +447,15 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTreeWithEmptyOptions = await categoryRepository.findTrees({}) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithEmptyOptions[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.should.be.eql([ { id: a1.id, @@ -464,6 +498,12 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTreeWithDepthOne = await categoryRepository.findTrees({ depth: 1 }) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithDepthOne[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithDepthOne.should.be.eql([ { id: a1.id, @@ -512,6 +552,13 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTree = await categoryRepository.findDescendantsTree(a1) + + // using sort because some drivers returns arrays in wrong order + categoriesTree.childCategories.sort((a, b) => a.id - b.id) + categoriesTree.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql({ id: a1.id, name: "a1", @@ -569,6 +616,13 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTree = await categoryRepository.findDescendantsTree(a1) + + // using sort because some drivers returns arrays in wrong order + categoriesTree.childCategories.sort((a, b) => a.id - b.id) + categoriesTree.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql({ id: a1.id, name: "a1", @@ -599,6 +653,15 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTreeWithEmptyOptions = await categoryRepository.findDescendantsTree(a1, {}) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithEmptyOptions.childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.should.be.eql({ id: a1.id, name: "a1", @@ -641,6 +704,12 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum await categoryRepository.findDescendantsTree(a1, { depth: 1, }) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithDepthOne.childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithDepthOne.should.be.eql({ id: a1.id, name: "a1", diff --git a/test/github-issues/8443/materialized-path/materialized-path.ts b/test/github-issues/8443/materialized-path/materialized-path.ts index 3c5b61820c..f33153e356 100644 --- a/test/github-issues/8443/materialized-path/materialized-path.ts +++ b/test/github-issues/8443/materialized-path/materialized-path.ts @@ -220,6 +220,13 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum await categoryRepository.save(a1) const categoriesTree = await categoryRepository.findTrees() + + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -290,6 +297,17 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum await categoryRepository.save(b1) const categoriesTree = await categoryRepository.findTrees() + + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree[1].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[1].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -364,6 +382,13 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum await categoryRepository.save(a1) const categoriesTree = await categoryRepository.findTrees() + + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -396,6 +421,15 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTreeWithEmptyOptions = await categoryRepository.findTrees({}) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithEmptyOptions[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.should.be.eql([ { id: a1.id, @@ -438,6 +472,12 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTreeWithDepthOne = await categoryRepository.findTrees({ depth: 1 }) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithDepthOne[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithDepthOne.should.be.eql([ { id: a1.id, @@ -486,6 +526,13 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTree = await categoryRepository.findDescendantsTree(a1) + + // using sort because some drivers returns arrays in wrong order + categoriesTree.childCategories.sort((a, b) => a.id - b.id) + categoriesTree.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql({ id: a1.id, name: "a1", @@ -543,6 +590,13 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTree = await categoryRepository.findDescendantsTree(a1) + + // using sort because some drivers returns arrays in wrong order + categoriesTree.childCategories.sort((a, b) => a.id - b.id) + categoriesTree.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql({ id: a1.id, name: "a1", @@ -573,6 +627,15 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTreeWithEmptyOptions = await categoryRepository.findDescendantsTree(a1, {}) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithEmptyOptions.childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.should.be.eql({ id: a1.id, name: "a1", @@ -615,6 +678,12 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum await categoryRepository.findDescendantsTree(a1, { depth: 1, }) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithDepthOne.childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithDepthOne.should.be.eql({ id: a1.id, name: "a1", diff --git a/test/github-issues/8443/nested-set/nested-set.ts b/test/github-issues/8443/nested-set/nested-set.ts index 2cbc683f15..0323ada60d 100644 --- a/test/github-issues/8443/nested-set/nested-set.ts +++ b/test/github-issues/8443/nested-set/nested-set.ts @@ -222,6 +222,13 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum await categoryRepository.save(a1) const categoriesTree = await categoryRepository.findTrees() + + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -280,6 +287,13 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum await categoryRepository.save(a1) const categoriesTree = await categoryRepository.findTrees() + + // using sort because some drivers returns arrays in wrong order + categoriesTree[0].childCategories.sort((a, b) => a.id - b.id) + categoriesTree[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql([ { id: a1.id, @@ -312,6 +326,15 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTreeWithEmptyOptions = await categoryRepository.findTrees({}) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithEmptyOptions[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.should.be.eql([ { id: a1.id, @@ -354,6 +377,15 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTreeWithDepthOne = await categoryRepository.findTrees({ depth: 1 }) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithDepthOne[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithDepthOne[0].childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithDepthOne.should.be.eql([ { id: a1.id, @@ -422,6 +454,13 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTree = await categoryRepository.findDescendantsTree(a1) + + // using sort because some drivers returns arrays in wrong order + categoriesTree.childCategories.sort((a, b) => a.id - b.id) + categoriesTree.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql({ id: a1.id, name: "a1", @@ -479,7 +518,13 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTree = await categoryRepository.findDescendantsTree(a1) - console.log(categoriesTree) + + // using sort because some drivers returns arrays in wrong order + categoriesTree.childCategories.sort((a, b) => a.id - b.id) + categoriesTree.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTree.should.be.eql({ id: a1.id, name: "a1", @@ -510,6 +555,15 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum const categoriesTreeWithEmptyOptions = await categoryRepository.findDescendantsTree(a1, {}) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithEmptyOptions.childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.childCategories[0].childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithEmptyOptions.should.be.eql({ id: a1.id, name: "a1", @@ -552,6 +606,12 @@ describe("github issues > #8443 QueryFailedError when tree entity with JoinColum await categoryRepository.findDescendantsTree(a1, { depth: 1, }) + + // using sort because some drivers returns arrays in wrong order + categoriesTreeWithDepthOne.childCategories.sort( + (a, b) => a.id - b.id, + ) + categoriesTreeWithDepthOne.should.be.eql({ id: a1.id, name: "a1", diff --git a/test/github-issues/85/issue-85.ts b/test/github-issues/85/issue-85.ts index a5aabba477..f01fd2b255 100644 --- a/test/github-issues/85/issue-85.ts +++ b/test/github-issues/85/issue-85.ts @@ -24,6 +24,9 @@ describe("github issues > #85 - Column option insert: false, update: false", () it("should ignore value of non-inserted column", () => Promise.all( connections.map(async (connection) => { + // Skip because test relies on DEFAULT values and Spanner does not support it + if (connection.driver.options.type === "spanner") return + const doc1 = new Document() doc1.id = 1 doc1.version = 42 @@ -37,6 +40,9 @@ describe("github issues > #85 - Column option insert: false, update: false", () it("should be able to create an entity with column entirely missing", () => Promise.all( connections.map(async (connection) => { + // Skip because test relies on DEFAULT values and Spanner does not support it + if (connection.driver.options.type === "spanner") return + // We delete the non-inserted column entirely, so that any use of it will throw an error. let queryRunner = connection.createQueryRunner() await queryRunner.dropColumn("document", "permission") diff --git a/test/github-issues/8522/entity/Role.ts b/test/github-issues/8522/entity/Role.ts index 73a676e957..0fad14f30e 100644 --- a/test/github-issues/8522/entity/Role.ts +++ b/test/github-issues/8522/entity/Role.ts @@ -3,7 +3,7 @@ import { TableInheritance, Column, Entity } from "../../../../src" import { BaseEntity } from "./BaseEntity" @Entity() -@TableInheritance({ column: { type: "varchar", name: "type" } }) +@TableInheritance({ column: { type: String, name: "type" } }) export class Role extends BaseEntity { @Column() name: string diff --git a/test/github-issues/8522/entity/User.ts b/test/github-issues/8522/entity/User.ts index 2df96311df..991a8dfaec 100644 --- a/test/github-issues/8522/entity/User.ts +++ b/test/github-issues/8522/entity/User.ts @@ -2,7 +2,7 @@ import { TableInheritance, Column, Entity } from "../../../../src" import { BaseEntity } from "./BaseEntity" @Entity() -@TableInheritance({ column: { type: "varchar", name: "type" } }) +@TableInheritance({ column: { type: String, name: "type" } }) export abstract class User extends BaseEntity { @Column() firstName: string diff --git a/test/github-issues/8690/entity/entities.ts b/test/github-issues/8690/entity/entities.ts index 550579abe3..fa155d7261 100644 --- a/test/github-issues/8690/entity/entities.ts +++ b/test/github-issues/8690/entity/entities.ts @@ -18,7 +18,7 @@ const WrappedIntTransformer = { @Entity() export class User { @PrimaryColumn({ - type: "int", + type: Number, transformer: WrappedIntTransformer, nullable: false, }) @@ -31,7 +31,7 @@ export class User { @Entity() export class Photo { @PrimaryColumn({ - type: "int", + type: Number, transformer: WrappedIntTransformer, nullable: false, }) @@ -41,7 +41,7 @@ export class Photo { url: string @Column({ - type: "int", + type: Number, transformer: WrappedIntTransformer, nullable: false, }) diff --git a/test/github-issues/8723/entity/Photo.ts b/test/github-issues/8723/entity/Photo.ts index fb102b636f..07d12c4a4e 100644 --- a/test/github-issues/8723/entity/Photo.ts +++ b/test/github-issues/8723/entity/Photo.ts @@ -9,7 +9,7 @@ import { User } from "./User" @Entity() export class Photo { - @PrimaryColumn({ type: "int", nullable: false }) + @PrimaryColumn({ nullable: false }) id: number @OneToOne(() => User, { nullable: true }) diff --git a/test/github-issues/8723/entity/User.ts b/test/github-issues/8723/entity/User.ts index d3ac2254f9..dbe64bede8 100644 --- a/test/github-issues/8723/entity/User.ts +++ b/test/github-issues/8723/entity/User.ts @@ -2,7 +2,7 @@ import { Column, Entity, PrimaryColumn } from "../../../../src" @Entity() export class User { - @PrimaryColumn({ type: "int", nullable: false }) + @PrimaryColumn({ nullable: false }) id: number @Column() diff --git a/test/other-issues/escaping-function-parameter/escaping-function-parameter.ts b/test/other-issues/escaping-function-parameter/escaping-function-parameter.ts index 626f0d208e..bb8dfc70df 100644 --- a/test/other-issues/escaping-function-parameter/escaping-function-parameter.ts +++ b/test/other-issues/escaping-function-parameter/escaping-function-parameter.ts @@ -70,6 +70,7 @@ describe("other issues > escaping function parameter", () => { .set({ title: () => "'super title'", }) + .where("id = :id", { id: post.id }) .execute() const loadedPost = await connection.manager.findOneBy(Post, { diff --git a/test/utils/test-utils.ts b/test/utils/test-utils.ts index ec3a04a664..7d7858d886 100644 --- a/test/utils/test-utils.ts +++ b/test/utils/test-utils.ts @@ -13,7 +13,6 @@ import { QueryResultCache } from "../../src/cache/QueryResultCache" import path from "path" import { ObjectUtils } from "../../src/util/ObjectUtils" import { EntitySubscriberMetadataArgs } from "../../src/metadata-args/EntitySubscriberMetadataArgs" -import { v4 as uuidv4 } from "uuid" /** * Interface in which data is stored in ormconfig.json of the project. @@ -300,45 +299,56 @@ export function setupTestingConnections( }) } -export function createDataSource(options: DataSourceOptions): DataSource { - class GeneratedColumnReplacerSubscriber - implements EntitySubscriberInterface - { - static globalIncrementValues: { [entityName: string]: number } = {} - beforeInsert(event: InsertEvent): Promise | void { - event.metadata.generatedColumns.map((column) => { - if (column.generationStrategy === "increment") { - if ( - !GeneratedColumnReplacerSubscriber - .globalIncrementValues[event.metadata.tableName] - ) { - GeneratedColumnReplacerSubscriber.globalIncrementValues[ - event.metadata.tableName - ] = 0 - } +class GeneratedColumnReplacerSubscriber implements EntitySubscriberInterface { + static globalIncrementValues: { [entityName: string]: number } = {} + beforeInsert(event: InsertEvent): Promise | void { + event.metadata.columns.map((column) => { + if (column.generationStrategy === "increment") { + if ( + !GeneratedColumnReplacerSubscriber.globalIncrementValues[ + event.metadata.tableName + ] + ) { GeneratedColumnReplacerSubscriber.globalIncrementValues[ event.metadata.tableName - ] += 1 - - column.setEntityValue( - event.entity, - GeneratedColumnReplacerSubscriber.globalIncrementValues[ - event.metadata.tableName - ], - ) - } else if (column.generationStrategy === "uuid") { - column.setEntityValue(event.entity, uuidv4()) + ] = 0 } - }) - } - } + GeneratedColumnReplacerSubscriber.globalIncrementValues[ + event.metadata.tableName + ] += 1 - // todo: uncomment later - if (options.type === ("spanner" as any)) { - getMetadataArgsStorage().entitySubscribers.push({ - target: GeneratedColumnReplacerSubscriber, - } as EntitySubscriberMetadataArgs) + column.setEntityValue( + event.entity, + GeneratedColumnReplacerSubscriber.globalIncrementValues[ + event.metadata.tableName + ], + ) + } else if ( + (column.isCreateDate || column.isUpdateDate) && + !column.getEntityValue(event.entity) + ) { + column.setEntityValue(event.entity, new Date()) + } else if ( + !column.isCreateDate && + !column.isUpdateDate && + !column.isVirtual && + column.default !== undefined && + column.getEntityValue(event.entity) === undefined + ) { + column.setEntityValue(event.entity, column.default) + } + }) + } +} +getMetadataArgsStorage().entitySubscribers.push({ + target: GeneratedColumnReplacerSubscriber, +} as EntitySubscriberMetadataArgs) +export function createDataSource(options: DataSourceOptions): DataSource { + if (options.type === "spanner") { + process.env.SPANNER_EMULATOR_HOST = "localhost:9010" + // process.env.GOOGLE_APPLICATION_CREDENTIALS = + // "/Users/messer/Documents/google/typeorm-spanner-3b57e071cbf0.json" if (Array.isArray(options.subscribers)) { options.subscribers.push( GeneratedColumnReplacerSubscriber as Function, @@ -479,6 +489,7 @@ export function closeTestingConnections(connections: DataSource[]) { * Reloads all databases for all given connections. */ export function reloadTestingDatabases(connections: DataSource[]) { + GeneratedColumnReplacerSubscriber.globalIncrementValues = {} return Promise.all( connections.map((connection) => connection.synchronize(true)), )