mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
feat(spanner): support insert returning (#11460)
* Update SpannerDriver.ts * Update package.json * Update package.json * Update package.json * Update package-lock.json * Update package.json * #11453 * Revert "Update package.json" This reverts commit 20f24de10cda62ad0c9a368b14290fbb4f355b32. * Revert "Update package.json" This reverts commit bcf6678e95b57570ea526935bb7490c9b11a16da. * Update package.json * Revert "Update package-lock.json" This reverts commit a003e5659336b38b8cade5f1605c17f7c3e59673. * #11460 * #11460 * FIX/Spanner Numeric type value string * #11460 Test functional spanner * test: update returning tests * refactor: simplify condition * style: fix lint/format * test: fix returning test for spanner --------- Co-authored-by: Lucian Mocanu <alumni@users.noreply.github.com>
This commit is contained in:
parent
e9eaf79604
commit
144634d4c0
@ -134,5 +134,16 @@
|
||||
"maxRetries": 3
|
||||
},
|
||||
"logging": false
|
||||
},
|
||||
{
|
||||
"skip": false,
|
||||
"name": "spanner",
|
||||
"type": "spanner",
|
||||
"host": "localhost",
|
||||
"projectId": "test-project",
|
||||
"instanceId": "test-instance",
|
||||
"databaseId": "test-db",
|
||||
"port": 9010,
|
||||
"logging": false
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Driver, ReturningType } from "../Driver"
|
||||
import { Driver } from "../Driver"
|
||||
import { DriverPackageNotInstalledError } from "../../error/DriverPackageNotInstalledError"
|
||||
import { SpannerQueryRunner } from "./SpannerQueryRunner"
|
||||
import { ObjectLiteral } from "../../common/ObjectLiteral"
|
||||
@ -178,16 +178,6 @@ export class SpannerDriver implements Driver {
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported returning types
|
||||
*/
|
||||
private readonly _isReturningSqlSupported: Record<ReturningType, boolean> =
|
||||
{
|
||||
delete: false,
|
||||
insert: false,
|
||||
update: false,
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
@ -352,7 +342,7 @@ export class SpannerDriver implements Driver {
|
||||
target: EntityMetadata | Table | View | TableForeignKey | string,
|
||||
): { database?: string; schema?: string; tableName: string } {
|
||||
const driverDatabase = this.database
|
||||
const driverSchema = undefined
|
||||
const driverSchema: any = undefined
|
||||
|
||||
if (target instanceof Table || target instanceof View) {
|
||||
const parsed = this.parseTableName(target.name)
|
||||
@ -412,7 +402,7 @@ export class SpannerDriver implements Driver {
|
||||
|
||||
if (columnMetadata.type === "numeric") {
|
||||
const lib = this.options.driver || PlatformTools.load("spanner")
|
||||
return lib.Spanner.numeric(value)
|
||||
return lib.Spanner.numeric(value.toString())
|
||||
} else if (columnMetadata.type === "date") {
|
||||
return DateUtils.mixedDateToDateString(value)
|
||||
} else if (columnMetadata.type === "json") {
|
||||
@ -707,15 +697,15 @@ export class SpannerDriver implements Driver {
|
||||
/**
|
||||
* Returns true if driver supports RETURNING / OUTPUT statement.
|
||||
*/
|
||||
isReturningSqlSupported(returningType: ReturningType): boolean {
|
||||
return this._isReturningSqlSupported[returningType]
|
||||
isReturningSqlSupported(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if driver supports uuid values generation on its own.
|
||||
*/
|
||||
isUUIDGenerationSupported(): boolean {
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -7,7 +7,7 @@ import { TypeORMError } from "./TypeORMError"
|
||||
export class ReturningStatementNotSupportedError extends TypeORMError {
|
||||
constructor() {
|
||||
super(
|
||||
`OUTPUT or RETURNING clause only supported by Microsoft SQL Server or PostgreSQL or MariaDB databases.`,
|
||||
`OUTPUT or RETURNING clause only supported by PostgreSQL, MariaDB, Microsoft SqlServer or Google Spanner.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,6 +289,9 @@ export class DeleteQueryBuilder<Entity extends ObjectLiteral>
|
||||
if (this.connection.driver.options.type === "mssql") {
|
||||
return `DELETE FROM ${tableName} OUTPUT ${returningExpression}${whereExpression}`
|
||||
}
|
||||
if (this.connection.driver.options.type === "spanner") {
|
||||
return `DELETE FROM ${tableName}${whereExpression} THEN RETURN ${returningExpression}`
|
||||
}
|
||||
return `DELETE FROM ${tableName}${whereExpression} RETURNING ${returningExpression}`
|
||||
}
|
||||
}
|
||||
|
||||
@ -650,6 +650,13 @@ export class InsertQueryBuilder<
|
||||
query += ` RETURNING ${returningExpression}`
|
||||
}
|
||||
|
||||
if (
|
||||
returningExpression &&
|
||||
this.connection.driver.options.type === "spanner"
|
||||
) {
|
||||
query += ` THEN RETURN ${returningExpression}`
|
||||
}
|
||||
|
||||
// Inserting a specific value for an auto-increment primary key in mssql requires enabling IDENTITY_INSERT
|
||||
// IDENTITY_INSERT can only be enabled for tables where there is an IDENTITY column and only if there is a value to be inserted (i.e. supplying DEFAULT is prohibited if IDENTITY_INSERT is enabled)
|
||||
if (
|
||||
@ -865,6 +872,13 @@ export class InsertQueryBuilder<
|
||||
this.connection.driver.normalizeDefault(
|
||||
column,
|
||||
)
|
||||
} else if (
|
||||
this.connection.driver.options.type ===
|
||||
"spanner" &&
|
||||
column.isGenerated &&
|
||||
column.generationStrategy === "uuid"
|
||||
) {
|
||||
expression += "GENERATE_UUID()" // Produces a random universally unique identifier (UUID) as a STRING value.
|
||||
} else {
|
||||
expression += "NULL" // otherwise simply use NULL and pray if column is nullable
|
||||
}
|
||||
|
||||
@ -153,22 +153,31 @@ export class ReturningResultsEntityUpdator {
|
||||
|
||||
const generatedMaps = entities.map((entity, entityIndex) => {
|
||||
if (
|
||||
this.queryRunner.connection.driver.options.type === "oracle" &&
|
||||
Array.isArray(insertResult.raw) &&
|
||||
this.expressionMap.extraReturningColumns.length > 0
|
||||
) {
|
||||
insertResult.raw = insertResult.raw.reduce(
|
||||
(newRaw, rawItem, rawItemIndex) => {
|
||||
newRaw[
|
||||
this.expressionMap.extraReturningColumns[
|
||||
rawItemIndex
|
||||
].databaseName
|
||||
] = rawItem[0]
|
||||
return newRaw
|
||||
},
|
||||
{} as ObjectLiteral,
|
||||
)
|
||||
if (
|
||||
this.queryRunner.connection.driver.options.type === "oracle"
|
||||
) {
|
||||
insertResult.raw = insertResult.raw.reduce(
|
||||
(newRaw, rawItem, rawItemIndex) => {
|
||||
newRaw[
|
||||
this.expressionMap.extraReturningColumns[
|
||||
rawItemIndex
|
||||
].databaseName
|
||||
] = rawItem[0]
|
||||
return newRaw
|
||||
},
|
||||
{} as ObjectLiteral,
|
||||
)
|
||||
} else if (
|
||||
this.queryRunner.connection.driver.options.type ===
|
||||
"spanner"
|
||||
) {
|
||||
insertResult.raw = insertResult.raw[0]
|
||||
}
|
||||
}
|
||||
|
||||
// get all values generated by a database for us
|
||||
const result = Array.isArray(insertResult.raw)
|
||||
? insertResult.raw[entityIndex]
|
||||
|
||||
@ -697,6 +697,14 @@ export class UpdateQueryBuilder<Entity extends ObjectLiteral>
|
||||
", ",
|
||||
)} OUTPUT ${returningExpression}${whereExpression}`
|
||||
}
|
||||
if (this.connection.driver.options.type === "spanner") {
|
||||
return `UPDATE ${this.getTableName(
|
||||
this.getMainTableName(),
|
||||
)} SET ${updateColumnAndValues.join(
|
||||
", ",
|
||||
)}${whereExpression} THEN RETURN ${returningExpression}`
|
||||
}
|
||||
|
||||
return `UPDATE ${this.getTableName(
|
||||
this.getMainTableName(),
|
||||
)} SET ${updateColumnAndValues.join(
|
||||
|
||||
10
test/functional/query-builder/returning/entity/User.ts
Normal file
10
test/functional/query-builder/returning/entity/User.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from "../../../../../src"
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
import { expect } from "chai"
|
||||
import "reflect-metadata"
|
||||
|
||||
import { DataSource } from "../../../../src/data-source/DataSource"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../../utils/test-utils"
|
||||
|
||||
import { User } from "./entity/User"
|
||||
|
||||
describe("query builder > insert/update/delete returning", () => {
|
||||
let connections: DataSource[]
|
||||
before(
|
||||
async () =>
|
||||
(connections = await createTestingConnections({
|
||||
entities: [__dirname + "/entity/*{.js,.ts}"],
|
||||
enabledDrivers: ["mssql", "postgres", "spanner"],
|
||||
})),
|
||||
)
|
||||
beforeEach(() => reloadTestingDatabases(connections))
|
||||
after(() => closeTestingConnections(connections))
|
||||
|
||||
it("should create and perform an INSERT statement, including RETURNING or OUTPUT clause", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const user = new User()
|
||||
user.name = "Tim Merrison"
|
||||
|
||||
const qb = connection
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(User)
|
||||
.values(user)
|
||||
.returning(
|
||||
connection.driver.options.type === "mssql"
|
||||
? "inserted.*"
|
||||
: "*",
|
||||
)
|
||||
|
||||
const sql = qb.getSql()
|
||||
if (connection.driver.options.type === "mssql") {
|
||||
expect(sql).to.equal(
|
||||
`INSERT INTO "user"("name") OUTPUT inserted.* VALUES (@0)`,
|
||||
)
|
||||
} else if (connection.driver.options.type === "postgres") {
|
||||
expect(sql).to.equal(
|
||||
`INSERT INTO "user"("name") VALUES ($1) RETURNING *`,
|
||||
)
|
||||
} else if (connection.driver.options.type === "spanner") {
|
||||
expect(sql).to.equal(
|
||||
"INSERT INTO `user`(`id`, `name`) VALUES (NULL, @param0) THEN RETURN *",
|
||||
)
|
||||
}
|
||||
|
||||
const returning = await qb.execute()
|
||||
expect(returning.raw).to.deep.equal([
|
||||
{ id: 1, name: user.name },
|
||||
])
|
||||
}),
|
||||
))
|
||||
|
||||
it("should create and perform an UPDATE statement, including RETURNING or OUTPUT clause", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const user = new User()
|
||||
user.name = "Tim Merrison"
|
||||
|
||||
await connection.manager.save(user)
|
||||
|
||||
const qb = connection
|
||||
.createQueryBuilder()
|
||||
.update(User)
|
||||
.set({ name: "Joe Bloggs" })
|
||||
.where("name = :name", { name: user.name })
|
||||
.returning(
|
||||
connection.driver.options.type === "mssql"
|
||||
? "inserted.*"
|
||||
: "*",
|
||||
)
|
||||
|
||||
const sql = qb.getSql()
|
||||
|
||||
if (connection.driver.options.type === "mssql") {
|
||||
expect(sql).to.equal(
|
||||
`UPDATE "user" SET "name" = @0 OUTPUT inserted.* WHERE "name" = @1`,
|
||||
)
|
||||
} else if (connection.driver.options.type === "postgres") {
|
||||
expect(sql).to.equal(
|
||||
`UPDATE "user" SET "name" = $1 WHERE "name" = $2 RETURNING *`,
|
||||
)
|
||||
} else if (connection.driver.options.type === "spanner") {
|
||||
expect(sql).to.equal(
|
||||
"UPDATE `user` SET `name` = @param0 WHERE `name` = @param1 THEN RETURN *",
|
||||
)
|
||||
}
|
||||
|
||||
const returning = await qb.execute()
|
||||
expect(returning.raw).to.deep.equal([
|
||||
{ id: 1, name: "Joe Bloggs" },
|
||||
])
|
||||
}),
|
||||
))
|
||||
|
||||
it("should create and perform a DELETE statement, including RETURNING or OUTPUT clause", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const user = new User()
|
||||
user.name = "Tim Merrison"
|
||||
|
||||
await connection.manager.save(user)
|
||||
|
||||
const qb = connection
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(User)
|
||||
.where("name = :name", { name: user.name })
|
||||
.returning(
|
||||
connection.driver.options.type === "mssql"
|
||||
? "deleted.*"
|
||||
: "*",
|
||||
)
|
||||
|
||||
const sql = qb.getSql()
|
||||
|
||||
if (connection.driver.options.type === "mssql") {
|
||||
expect(sql).to.equal(
|
||||
`DELETE FROM "user" OUTPUT deleted.* WHERE "name" = @0`,
|
||||
)
|
||||
} else if (connection.driver.options.type === "postgres") {
|
||||
expect(sql).to.equal(
|
||||
`DELETE FROM "user" WHERE "name" = $1 RETURNING *`,
|
||||
)
|
||||
} else if (connection.driver.options.type === "spanner") {
|
||||
expect(sql).to.equal(
|
||||
"DELETE FROM `user` WHERE `name` = @param0 THEN RETURN *",
|
||||
)
|
||||
}
|
||||
|
||||
const returning = await qb.execute()
|
||||
expect(returning.raw).to.deep.equal([
|
||||
{ id: 1, name: user.name },
|
||||
])
|
||||
}),
|
||||
))
|
||||
})
|
||||
@ -1,12 +0,0 @@
|
||||
import { Entity } from "../../../../src/decorator/entity/Entity"
|
||||
import { PrimaryGeneratedColumn } from "../../../../src/decorator/columns/PrimaryGeneratedColumn"
|
||||
import { Column } from "../../../../src/decorator/columns/Column"
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
}
|
||||
@ -1,219 +0,0 @@
|
||||
import "reflect-metadata"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../utils/test-utils"
|
||||
import { DataSource } from "../../../src/data-source/DataSource"
|
||||
import { User } from "./entity/User"
|
||||
import { expect } from "chai"
|
||||
import { ReturningStatementNotSupportedError } from "../../../src/error/ReturningStatementNotSupportedError"
|
||||
|
||||
describe("github issues > #660 Specifying a RETURNING or OUTPUT clause with QueryBuilder", () => {
|
||||
let connections: DataSource[]
|
||||
before(
|
||||
async () =>
|
||||
(connections = await createTestingConnections({
|
||||
entities: [__dirname + "/entity/*{.js,.ts}"],
|
||||
})),
|
||||
)
|
||||
beforeEach(() => reloadTestingDatabases(connections))
|
||||
after(() => closeTestingConnections(connections))
|
||||
|
||||
it("should create an INSERT statement, including RETURNING or OUTPUT clause (PostgreSQL and MSSQL only)", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const user = new User()
|
||||
user.name = "Tim Merrison"
|
||||
|
||||
let sql: string = ""
|
||||
try {
|
||||
sql = connection
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(User)
|
||||
.values(user)
|
||||
.returning(
|
||||
connection.driver.options.type === "postgres"
|
||||
? "*"
|
||||
: "inserted.*",
|
||||
)
|
||||
.disableEscaping()
|
||||
.getSql()
|
||||
} catch (err) {
|
||||
expect(err.message).to.eql(
|
||||
new ReturningStatementNotSupportedError().message,
|
||||
)
|
||||
}
|
||||
|
||||
if (connection.driver.options.type === "mssql") {
|
||||
expect(sql).to.equal(
|
||||
"INSERT INTO user(name) OUTPUT inserted.* VALUES (@0)",
|
||||
)
|
||||
} else if (connection.driver.options.type === "postgres") {
|
||||
expect(sql).to.equal(
|
||||
"INSERT INTO user(name) VALUES ($1) RETURNING *",
|
||||
)
|
||||
}
|
||||
}),
|
||||
))
|
||||
|
||||
it("should perform insert with RETURNING or OUTPUT clause (PostgreSQL and MSSQL only)", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const user = new User()
|
||||
user.name = "Tim Merrison"
|
||||
|
||||
if (
|
||||
connection.driver.options.type === "mssql" ||
|
||||
connection.driver.options.type === "postgres"
|
||||
) {
|
||||
const returning = await connection
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(User)
|
||||
.values(user)
|
||||
.returning(
|
||||
connection.driver.options.type === "postgres"
|
||||
? "*"
|
||||
: "inserted.*",
|
||||
)
|
||||
.execute()
|
||||
|
||||
returning.raw.should.be.eql([{ id: 1, name: user.name }])
|
||||
}
|
||||
}),
|
||||
))
|
||||
|
||||
it("should create an UPDATE statement, including RETURNING or OUTPUT clause (PostgreSQL and MSSQL only)", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const user = new User()
|
||||
user.name = "Tim Merrison"
|
||||
|
||||
try {
|
||||
const sql = connection
|
||||
.createQueryBuilder()
|
||||
.update(User)
|
||||
.set({ name: "Joe Bloggs" })
|
||||
.where("name = :name", { name: user.name })
|
||||
.returning(
|
||||
connection.driver.options.type === "postgres"
|
||||
? "*"
|
||||
: "inserted.*",
|
||||
)
|
||||
.disableEscaping()
|
||||
.getSql()
|
||||
|
||||
if (connection.driver.options.type === "mssql") {
|
||||
expect(sql).to.equal(
|
||||
"UPDATE user SET name = @0 OUTPUT inserted.* WHERE name = @1",
|
||||
)
|
||||
} else if (connection.driver.options.type === "postgres") {
|
||||
expect(sql).to.equal(
|
||||
"UPDATE user SET name = $1 WHERE name = $2 RETURNING *",
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
expect(err.message).to.eql(
|
||||
new ReturningStatementNotSupportedError().message,
|
||||
)
|
||||
}
|
||||
}),
|
||||
))
|
||||
|
||||
it("should perform update with RETURNING or OUTPUT clause (PostgreSQL and MSSQL only)", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const user = new User()
|
||||
user.name = "Tim Merrison"
|
||||
|
||||
await connection.manager.save(user)
|
||||
|
||||
if (
|
||||
connection.driver.options.type === "mssql" ||
|
||||
connection.driver.options.type === "postgres"
|
||||
) {
|
||||
const returning = await connection
|
||||
.createQueryBuilder()
|
||||
.update(User)
|
||||
.set({ name: "Joe Bloggs" })
|
||||
.where("name = :name", { name: user.name })
|
||||
.returning(
|
||||
connection.driver.options.type === "postgres"
|
||||
? "*"
|
||||
: "inserted.*",
|
||||
)
|
||||
.execute()
|
||||
|
||||
returning.raw.should.be.eql([{ id: 1, name: "Joe Bloggs" }])
|
||||
}
|
||||
}),
|
||||
))
|
||||
|
||||
it("should create a DELETE statement, including RETURNING or OUTPUT clause (PostgreSQL and MSSQL only)", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
try {
|
||||
const user = new User()
|
||||
user.name = "Tim Merrison"
|
||||
|
||||
const sql = connection
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(User)
|
||||
.where("name = :name", { name: user.name })
|
||||
.returning(
|
||||
connection.driver.options.type === "postgres"
|
||||
? "*"
|
||||
: "deleted.*",
|
||||
)
|
||||
.disableEscaping()
|
||||
.getSql()
|
||||
|
||||
if (connection.driver.options.type === "mssql") {
|
||||
expect(sql).to.equal(
|
||||
"DELETE FROM user OUTPUT deleted.* WHERE name = @0",
|
||||
)
|
||||
} else if (connection.driver.options.type === "postgres") {
|
||||
expect(sql).to.equal(
|
||||
"DELETE FROM user WHERE name = $1 RETURNING *",
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
expect(err.message).to.eql(
|
||||
new ReturningStatementNotSupportedError().message,
|
||||
)
|
||||
}
|
||||
}),
|
||||
))
|
||||
|
||||
it("should perform delete with RETURNING or OUTPUT clause (PostgreSQL and MSSQL only)", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const user = new User()
|
||||
user.name = "Tim Merrison"
|
||||
|
||||
await connection.manager.save(user)
|
||||
|
||||
if (
|
||||
connection.driver.options.type === "mssql" ||
|
||||
connection.driver.options.type === "postgres"
|
||||
) {
|
||||
const returning = await connection
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(User)
|
||||
.where("name = :name", { name: user.name })
|
||||
.returning(
|
||||
connection.driver.options.type === "postgres"
|
||||
? "*"
|
||||
: "deleted.*",
|
||||
)
|
||||
.execute()
|
||||
|
||||
returning.raw.should.be.eql([{ id: 1, name: user.name }])
|
||||
}
|
||||
}),
|
||||
))
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user