feat: add json type support for Oracle (#10611)

* refactor: remove oracle docker tests and update DEVELOPER.md

Since oracle runs at thin mode now. Now extra docker tests are needed

* chore: increase oracle version to 21c

* feat: add json column types to oracle

* fix: try to resolve pipeline issue to increase oracle waiting time

* fix: try remove networks from oracle to fix pipeline

* fix: add container name

* fix: add missing oracledb driver in package-lock.json

* fix: corrected tests

* fix: remove tests, since only work with old oracle db

* fix: correct tests

* fix: remove deprecated types

* fix: add missing grant for materialized views

* fix: oracle-isolation.ts test

* fix: issue-3363.ts test

* fix: schema in tests
This commit is contained in:
ertl 2024-01-26 06:27:28 +01:00 committed by GitHub
parent 4493db4d1b
commit 7e85460f10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 113 additions and 257 deletions

View File

@ -51,7 +51,6 @@ commands:
command: |
if [ ! -d node_modules ]; then
npm install
npm install oracledb
fi
- save_cache:
name: Save node_modules cache
@ -126,6 +125,9 @@ jobs:
- run:
name: Wait for Services to be Available
command: |
DEFAULT_WAIT_TIME=60
ORACLE_WAIT_TIME=120
COMMANDS=$(
cat ormconfig.json \
| jq -r '
@ -139,31 +141,15 @@ jobs:
)
echo "Running '$COMMANDS'"
WAIT_TIME=$([ ! -z "$(jq -r '.[] | select(.skip == false and .name == "oracle")' ormconfig.json)" ] && echo "$ORACLE_WAIT_TIME" || echo "$DEFAULT_WAIT_TIME")
if [ ! -z "$COMMANDS" ]; then
docker run \
--network typeorm_default \
--tty \
ubuntu:trusty \
timeout 60 sh -c "until ($COMMANDS); do echo \"Waiting for Services to be Available ...\"; sleep 5; done"
timeout $WAIT_TIME sh -c "until ($COMMANDS); do echo \"Waiting for Services to be Available ...\"; sleep 5; done"
fi
- run:
name: "Wait for OracleDB to be Available"
command: |
COMMANDS=$(
cat ormconfig.json \
| jq -r '
map(select(.skip == false)
| select(.name == "oracle")
| "sleep 60"
)
| join(" && ")
'
)
if [ ! -z "$COMMANDS" ]; then
echo "$COMMANDS seconds to wait for oracledb";
$COMMANDS
fi
# Download and cache dependencies
- run:
name: "Run Tests with Coverage"

View File

@ -57,10 +57,6 @@ Install all TypeORM dependencies by running this command:
npm install
```
During installation, you may have some problems with some dependencies.
For example to properly install oracle driver you need to follow all instructions from
[node-oracle documentation](https://github.com/oracle/node-oracledb).
## ORM config
To create an initial `ormconfig.json` file, run the following command:
@ -174,41 +170,3 @@ in the root of the project. Once all images are fetched and run you can run test
- The docker image of mssql-server needs at least 3.25GB of RAM.
- Make sure to assign enough memory to the Docker VM if you're running on Docker for Mac or Windows
### Oracle XE
In order to run tests on Oracle XE locally, we need to start 2 docker containers:
- a container with Oracle XE database
- a container with typeorm and its tests
#### 1. Booting Oracle XE database
Execute in shell the next command:
```shell
docker-compose up -d oracle
```
It will start an oracle instance only.
The instance will be run in background,
therefore, we need to stop it later on.
#### 2. Booting typeorm for Oracle
Execute in shell the next command:
```shell
docker-compose -f docker-compose.oracle.yml up
```
it will start a nodejs instance which builds typeorm and executes unit tests.
The instance exits after the run.
#### 3. Shutting down Oracle XE database
Execute in shell the next command:
```shell
docker-compose down
```

View File

@ -1,15 +0,0 @@
version: "3"
services:
oracle-test:
build:
context: docker/oracle
volumes:
- .:/typeorm
- /typeorm/build
- /typeorm/node_modules
networks:
- typeorm
networks:
typeorm:

View File

@ -57,14 +57,18 @@ services:
ports:
- "26257:26257"
# oracle
oracle:
image: imnotjames/oracle-xe:18
build:
context: docker/oracle
container_name: "typeorm-oracle"
ports:
- "1521:1521"
networks:
- default
- typeorm
#volumes:
# - oracle-data:/opt/oracle/oradata
healthcheck:
test: [ "CMD", "/opt/oracle/checkDBStatus.sh" ]
interval: 2s
# google cloud spanner
spanner:
@ -118,6 +122,3 @@ services:
#volumes:
# volume-hana-xe:
# mysql8_volume:
networks:
typeorm:

22
docker/oracle/01_init.sql Normal file
View File

@ -0,0 +1,22 @@
ALTER SESSION SET CONTAINER = XEPDB1;
CREATE TABLESPACE typeormspace32
DATAFILE 'typeormspace32.dbf'
SIZE 100M
AUTOEXTEND ON;
-- create users:
CREATE USER typeorm IDENTIFIED BY "oracle" DEFAULT TABLESPACE typeormspace32;
GRANT CREATE SESSION TO typeorm;
GRANT CREATE TABLE TO typeorm;
GRANT CREATE VIEW TO typeorm;
GRANT CREATE MATERIALIZED VIEW TO typeorm;
GRANT CREATE PROCEDURE TO typeorm;
GRANT CREATE SEQUENCE TO typeorm;
ALTER USER typeorm QUOTA UNLIMITED ON typeormspace32;
-- set password expiry to unlimited
ALTER PROFILE DEFAULT LIMIT PASSWORD_REUSE_TIME UNLIMITED;
ALTER PROFILE DEFAULT LIMIT PASSWORD_LIFE_TIME UNLIMITED;

View File

@ -1,18 +1,8 @@
FROM node:12
FROM container-registry.oracle.com/database/express:21.3.0-xe
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -qq -y install libaio1 && \
apt-get -q -y autoremove && \
rm -Rf /var/lib/apt/lists/*
ENV ORACLE_PWD=oracle
ENV ORACLE_SID=XE
COPY 01_init.sql /docker-entrypoint-initdb.d/startup/
ENV PORT=1521
WORKDIR /typeorm
ENTRYPOINT ["/docker-entrypoint.sh"]
COPY . /
RUN chmod 0755 /docker-entrypoint.sh
ENV PATH="$PATH:/typeorm/node_modules/.bin"
ENV LD_LIBRARY_PATH="/typeorm/node_modules/oracledb/instantclient_19_8/:$LD_LIBRARY_PATH"
ENV BLOB_URL="https://download.oracle.com/otn_software/linux/instantclient/19800/instantclient-basiclite-linux.x64-19.8.0.0.0dbru.zip"
CMD ["npm", "run", "test-fast"]
EXPOSE ${PORT}

View File

@ -1,16 +0,0 @@
[
{
"skip": false,
"name": "oracle",
"type": "oracle",
"host": "typeorm-oracle",
"username": "system",
"password": "oracle",
"port": 1521,
"sid": "XE",
"logging": false,
"extra": {
"connectString": "typeorm-oracle:1521/XE"
}
}
]

View File

@ -1,24 +0,0 @@
#!/usr/bin/env bash
# exit when any command fails
set -e
if [[ $INSTALL == 0 ]] && [[ ! -f ./package.json ]]; then
exit 0
fi
INSTALL=0
if [[ $INSTALL == 0 ]] && [[ "$(ls ./node_modules/ | wc -l | tr -d '\n')" == '0' ]]; then
INSTALL=1
fi
if [[ $INSTALL == 0 ]] && [[ ! -f ./node_modules/.md5 ]]; then
INSTALL=1
fi
if [[ $INSTALL == 0 ]] && ! md5sum --check ./node_modules/.md5; then
INSTALL=1
fi
if [[ $INSTALL == 1 ]]; then
npm ci --no-optional --ignore-scripts
md5sum ./package-lock.json > ./node_modules/.md5
fi

View File

@ -1,12 +0,0 @@
#!/usr/bin/env bash
# exit when any command fails
set -e
if [ ! -d node_modules/oracledb/instantclient_19_8 ]; then
curl -sf -o node_modules/oracledb/instantclient.zip $BLOB_URL
unzip -qqo node_modules/oracledb/instantclient.zip -d node_modules/oracledb/
rm node_modules/oracledb/instantclient.zip
cp /lib/*/libaio.so.* node_modules/oracledb/instantclient_19_8/
fi

View File

@ -1,12 +0,0 @@
#!/usr/bin/env bash
# exit when any command fails
set -e
npx rimraf build/compiled
npx tsc
cp /config/ormconfig.json build/compiled/ormconfig.json
if [ ! -f ormconfig.json ]; then
cp ormconfig.json.dist ormconfig.json
fi

View File

@ -1,43 +0,0 @@
#!/usr/bin/env bash
set -e
child_pid=0
parent_pid=$$
catch_exits() {
echo "${0}:stopping ${child_pid}"
kill ${child_pid} &
wait
echo "${0}:stopped ${child_pid}"
echo "${0}:exit"
exit 0
}
trap catch_exits TERM KILL INT SIGTERM SIGINT SIGKILL
fork() {
printf "'%s' " "${@}" | xargs -d "\n" -t sh -c
}
if [[ ! "${ENTRYPOINT_SKIP}" ]]; then
for file in `ls -v /docker-entrypoint.d/*.sh`
do
echo "${file}:starting"
fork ${file} &
child_pid=$!
echo "${file}:pid ${child_pid}"
wait ${child_pid}
echo "${file}:stopped ${child_pid}"
if [[ "${?}" != "0" ]]; then
exit 1;
fi
done
fi
echo "${0}:starting"
fork "${@}" &
child_pid=$!
echo "${0}:pid ${child_pid}"
wait ${child_pid}
echo "${0}:stopped ${child_pid}"

View File

@ -115,12 +115,12 @@
"type": "oracle",
"host": "typeorm-oracle",
"port": 1521,
"sid": "XE",
"username": "system",
"serviceName": "XEPDB1",
"username": "typeorm",
"password": "oracle",
"logging": false,
"extra": {
"connectString": "typeorm-oracle:1521/XE"
"connectString": "typeorm-oracle:1521/XEPDB1"
}
}
]

View File

@ -69,14 +69,14 @@
}
},
{
"skip": true,
"skip": false,
"name": "oracle",
"type": "oracle",
"host": "localhost",
"username": "system",
"username": "typeorm",
"password": "oracle",
"port": 1521,
"sid": "xe.oracle.docker",
"serviceName": "XEPDB1",
"logging": false
},
{

11
package-lock.json generated
View File

@ -10647,6 +10647,17 @@
"node": ">= 0.8.0"
}
},
"node_modules/oracledb": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/oracledb/-/oracledb-6.3.0.tgz",
"integrity": "sha512-fr3U66QxgGXb5cs/ozLBQU50TMbZcBQEWvSaj2rJAXG8KRrsZcGOK8JTlZL1yJHeW8cSjOm6n/wTw3SJksGjDg==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=14.6"
}
},
"node_modules/ordered-read-streams": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz",

View File

@ -128,6 +128,8 @@ export class OracleDriver implements Driver {
"nclob",
"rowid",
"urowid",
"simple-json",
"json",
]
/**
@ -299,8 +301,8 @@ export class OracleDriver implements Driver {
* either create a pool and create connection when needed.
*/
async connect(): Promise<void> {
this.oracle.fetchAsString = [this.oracle.CLOB]
this.oracle.fetchAsBuffer = [this.oracle.BLOB]
this.oracle.fetchAsString = [this.oracle.DB_TYPE_CLOB]
this.oracle.fetchAsBuffer = [this.oracle.DB_TYPE_BLOB]
if (this.options.replication) {
this.slaves = await Promise.all(
this.options.replication.slaves.map((slave) => {
@ -547,6 +549,8 @@ export class OracleDriver implements Driver {
return DateUtils.simpleArrayToString(value)
} else if (columnMetadata.type === "simple-json") {
return DateUtils.simpleJsonToString(value)
} else if (columnMetadata.type === "json") {
return DateUtils.simpleJsonToString(value)
}
return value
@ -577,8 +581,6 @@ export class OracleDriver implements Driver {
columnMetadata.type === "timestamp with local time zone"
) {
value = DateUtils.normalizeHydratedDate(value)
} else if (columnMetadata.type === "json") {
value = JSON.parse(value)
} else if (columnMetadata.type === "simple-array") {
value = DateUtils.stringToSimpleArray(value)
} else if (columnMetadata.type === "simple-json") {
@ -635,6 +637,8 @@ export class OracleDriver implements Driver {
return "clob"
} else if (column.type === "simple-json") {
return "clob"
} else if (column.type === "json") {
return "json"
} else {
return (column.type as string) || ""
}
@ -954,21 +958,24 @@ export class OracleDriver implements Driver {
case "smallint":
case "dec":
case "decimal":
return this.oracle.NUMBER
return this.oracle.DB_TYPE_NUMBER
case "char":
case "nchar":
case "nvarchar2":
case "varchar2":
return this.oracle.STRING
return this.oracle.DB_TYPE_VARCHAR
case "blob":
return this.oracle.BLOB
return this.oracle.DB_TYPE_BLOB
case "simple-json":
case "clob":
return this.oracle.CLOB
return this.oracle.DB_TYPE_CLOB
case "date":
case "timestamp":
case "timestamp with time zone":
case "timestamp with local time zone":
return this.oracle.DATE
return this.oracle.DB_TYPE_TIMESTAMP
case "json":
return this.oracle.DB_TYPE_JSON
}
}

View File

@ -214,7 +214,7 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
try {
const executionOptions = {
autoCommit: !this.isTransactionActive,
outFormat: this.driver.oracle.OBJECT,
outFormat: this.driver.oracle.OUT_FORMAT_OBJECT,
}
const raw = await databaseConnection.execute(
@ -323,7 +323,7 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
const executionOptions = {
autoCommit: !this.isTransactionActive,
outFormat: this.driver.oracle.OBJECT,
outFormat: this.driver.oracle.OUT_FORMAT_OBJECT,
}
const databaseConnection = await this.connect()

View File

@ -27,6 +27,8 @@ describe("database schema > column types > oracle", () => {
const postRepository = connection.getRepository(Post)
const queryRunner = connection.createQueryRunner()
const table = await queryRunner.getTable("post")
const simpleJson = { id: 1, name: "simple-json" }
const json = { id: 1, name: "json" }
await queryRunner.release()
const post = new Post()
@ -60,12 +62,14 @@ describe("database schema > column types > oracle", () => {
post.clob = "This is clob"
post.nclob = "This is nclob"
post.simpleArray = ["A", "B", "C"]
post.simpleJson = simpleJson
post.json = json
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.number.should.be.equal(post.number)
loadedPost.numeric.should.be.equal(post.numeric)
@ -105,6 +109,8 @@ describe("database schema > column types > oracle", () => {
loadedPost.simpleArray[1].should.be.equal(post.simpleArray[1])
loadedPost.simpleArray[2].should.be.equal(post.simpleArray[2])
loadedPost.simpleJson.should.be.deep.equal(simpleJson)
loadedPost.json.should.be.deep.equal(json)
table!.findColumnByName("id")!.type.should.be.equal("number")
table!
.findColumnByName("name")!
@ -158,6 +164,10 @@ describe("database schema > column types > oracle", () => {
table!
.findColumnByName("simpleArray")!
.type.should.be.equal("clob")
table!
.findColumnByName("simpleJson")!
.type.should.be.equal("clob")
table!.findColumnByName("json")!.type.should.be.equal("json")
}),
))

View File

@ -98,10 +98,15 @@ export class Post {
@Column("nclob")
nclob: string
@Column("json")
json: any
// -------------------------------------------------------------------------
// TypeOrm Specific Type
// -------------------------------------------------------------------------
@Column("simple-array")
simpleArray: string[]
@Column("simple-json")
simpleJson: any
}

View File

@ -1,30 +0,0 @@
import "reflect-metadata"
import { expect } from "chai"
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../../../utils/test-utils"
import { DataSource } from "../../../../../src/data-source"
describe("LegacyOracleNamingStrategy > create table using default naming strategy", () => {
let connections: DataSource[]
before(
async () =>
(connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["oracle"],
})),
)
// without reloadTestingDatabases(connections) -> tables should be created later
after(() => closeTestingConnections(connections))
it("should not create the table and fail due to ORA-00972", () =>
Promise.all(
connections.map(async (connection) => {
await expect(
reloadTestingDatabases([connection]),
).to.be.rejectedWith(/ORA-00972/gi)
}),
))
})

View File

@ -69,6 +69,15 @@ describe("transaction > transaction with oracle connection partial isolation sup
let postId: number | undefined = undefined,
categoryId: number | undefined = undefined
// Initial inserts are required to prevent ORA-08177 errors in Oracle 21c when using a serializable connection
// immediately after DDL statements. This ensures proper synchronization and helps avoid conflicts.
await connection.manager
.getRepository(Post)
.save({ title: "Post #0" })
await connection.manager
.getRepository(Category)
.save({ name: "Category #0" })
await connection.manager.transaction(
"SERIALIZABLE",
async (entityManager) => {

View File

@ -76,6 +76,15 @@ describe("github issues > #3363 Isolation Level in transaction() from Connection
let postId: number | undefined = undefined,
categoryId: number | undefined = undefined
// Initial inserts are required to prevent ORA-08177 errors in Oracle 21c when using a serializable connection
// immediately after DDL statements. This ensures proper synchronization and helps avoid conflicts.
await connection.manager
.getRepository(Post)
.save({ title: "Post #0" })
await connection.manager
.getRepository(Category)
.save({ name: "Category #0" })
await connection.transaction(
"SERIALIZABLE",
async (entityManager) => {

View File

@ -4,7 +4,7 @@ import {
UpdateDateColumn,
} from "../../../../src"
@Entity({ name: "foo", schema: "SYSTEM" })
@Entity({ name: "foo", schema: "TYPEORM" })
export class Foo {
@PrimaryGeneratedColumn({ name: "id" })
id: number

View File

@ -3,7 +3,7 @@ import { Foo } from "./Foo"
@ViewEntity({
name: "foo_view",
schema: "SYSTEM",
schema: "TYPEORM",
expression: (connection: DataSource) =>
connection.createQueryBuilder(Foo, "foo").select(`foo.updatedAt`),
})