refactor: database server version fetching & comparison (#11357)

This commit is contained in:
Lucian Mocanu 2025-03-26 11:41:07 +01:00 committed by GitHub
parent 834e85692f
commit 4e31a8648a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 214 additions and 173 deletions

View File

@ -34,10 +34,7 @@ export class DriverUtils {
}
static isReleaseVersionOrGreater(driver: Driver, version: string): boolean {
return (
driver.version != null &&
VersionUtils.isGreaterOrEqual(driver.version, version)
)
return VersionUtils.isGreaterOrEqual(driver.version, version)
}
static isPostgresFamily(driver: Driver): boolean {

View File

@ -361,7 +361,7 @@ export class AuroraMysqlDriver implements Driver {
*/
async connect(): Promise<void> {
if (!this.database) {
const queryRunner = await this.createQueryRunner("master")
const queryRunner = this.createQueryRunner("master")
this.database = await queryRunner.getCurrentDatabase()

View File

@ -305,7 +305,7 @@ export class CockroachDriver implements Driver {
}
if (!this.database || !this.searchSchema) {
const queryRunner = await this.createQueryRunner("master")
const queryRunner = this.createQueryRunner("master")
if (!this.database) {
this.database = await queryRunner.getCurrentDatabase()

View File

@ -2873,7 +2873,9 @@ export class CockroachQueryRunner
// we throw original error even if rollback thrown an error
if (!isAnotherTransactionActive)
await this.rollbackTransaction()
} catch (rollbackError) {}
} catch {
// no-op
}
throw error
}
}
@ -3739,12 +3741,13 @@ export class CockroachQueryRunner
/**
* Loads Cockroachdb version.
*/
protected async getVersion(): Promise<string> {
const result = await this.query(`SELECT version()`)
return result[0]["version"].replace(
/^CockroachDB CCL v([\d.]+) .*$/,
"$1",
async getVersion(): Promise<string> {
const result: [{ version: string }] = await this.query(
`SELECT version() AS "version"`,
)
const versionString = result[0].version
return versionString.replace(/^CockroachDB CCL v([\d.]+) .*$/, "$1")
}
/**

View File

@ -403,7 +403,7 @@ export class MysqlDriver implements Driver {
}
if (!this.database) {
const queryRunner = await this.createQueryRunner("master")
const queryRunner = this.createQueryRunner("master")
this.database = await queryRunner.getCurrentDatabase()
@ -411,28 +411,24 @@ export class MysqlDriver implements Driver {
}
const queryRunner = this.createQueryRunner("master")
const result: {
version: string
}[] = await queryRunner.query(`SELECT VERSION() AS \`version\``)
const dbVersion = result[0].version
this.version = dbVersion
this.version = await queryRunner.getVersion()
await queryRunner.release()
if (this.options.type === "mariadb") {
if (VersionUtils.isGreaterOrEqual(dbVersion, "10.0.5")) {
if (VersionUtils.isGreaterOrEqual(this.version, "10.0.5")) {
this._isReturningSqlSupported.delete = true
}
if (VersionUtils.isGreaterOrEqual(dbVersion, "10.5.0")) {
if (VersionUtils.isGreaterOrEqual(this.version, "10.5.0")) {
this._isReturningSqlSupported.insert = true
}
if (VersionUtils.isGreaterOrEqual(dbVersion, "10.2.0")) {
if (VersionUtils.isGreaterOrEqual(this.version, "10.2.0")) {
this.cteCapabilities.enabled = true
}
if (VersionUtils.isGreaterOrEqual(dbVersion, "10.7.0")) {
if (VersionUtils.isGreaterOrEqual(this.version, "10.7.0")) {
this.uuidColumnTypeSuported = true
}
} else if (this.options.type === "mysql") {
if (VersionUtils.isGreaterOrEqual(dbVersion, "8.0.0")) {
if (VersionUtils.isGreaterOrEqual(this.version, "8.0.0")) {
this.cteCapabilities.enabled = true
}
}
@ -735,7 +731,7 @@ export class MysqlDriver implements Driver {
} else if (
column.type === "json" &&
this.options.type === "mariadb" &&
!VersionUtils.isGreaterOrEqual(this.version ?? "0.0.0", "10.4.3")
!VersionUtils.isGreaterOrEqual(this.version, "10.4.3")
) {
/*
* MariaDB implements this as a LONGTEXT rather, as the JSON data type contradicts the SQL standard,

View File

@ -2555,7 +2555,7 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner {
])
const isMariaDb = this.driver.options.type === "mariadb"
const dbVersion = await this.getVersion()
const dbVersion = this.driver.version
// create tables for loaded tables
return Promise.all(
@ -3366,9 +3366,19 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner {
return c
}
protected async getVersion(): Promise<string> {
const result = await this.query(`SELECT VERSION() AS \`version\``)
return result[0]["version"]
async getVersion(): Promise<string> {
const result: [{ version: string }] = await this.query(
`SELECT VERSION() AS \`version\``,
)
// MariaDB: https://mariadb.com/kb/en/version/
// - "10.2.27-MariaDB-10.2.27+maria~jessie-log"
// MySQL: https://dev.mysql.com/doc/refman/8.4/en/information-functions.html#function_version
// - "8.4.3"
// - "8.4.4-standard"
const versionString = result[0].version
return versionString.replace(/^([\d.]+).*$/, "$1")
}
/**

View File

@ -318,7 +318,7 @@ export class OracleDriver implements Driver {
}
if (!this.database || !this.schema) {
const queryRunner = await this.createQueryRunner("master")
const queryRunner = this.createQueryRunner("master")
if (!this.database) {
this.database = await queryRunner.getCurrentDatabase()

View File

@ -358,20 +358,20 @@ export class PostgresDriver implements Driver {
this.master = await this.createPool(this.options, this.options)
}
if (!this.database || !this.searchSchema) {
const queryRunner = await this.createQueryRunner("master")
const queryRunner = this.createQueryRunner("master")
if (!this.database) {
this.database = await queryRunner.getCurrentDatabase()
}
this.version = await queryRunner.getVersion()
if (!this.searchSchema) {
this.searchSchema = await queryRunner.getCurrentSchema()
}
await queryRunner.release()
if (!this.database) {
this.database = await queryRunner.getCurrentDatabase()
}
if (!this.searchSchema) {
this.searchSchema = await queryRunner.getCurrentSchema()
}
await queryRunner.release()
if (!this.schema) {
this.schema = this.searchSchema
}
@ -391,21 +391,8 @@ export class PostgresDriver implements Driver {
await this.enableExtensions(extensionsMetadata, connection)
}
const results = (await this.executeQuery(
connection,
"SELECT version();",
)) as {
rows: {
version: string
}[]
}
const versionString = results.rows[0].version.replace(
/^PostgreSQL ([\d.]+) .*$/,
"$1",
)
this.version = versionString
this.isGeneratedColumnsSupported = VersionUtils.isGreaterOrEqual(
versionString,
this.version,
"12.0",
)
@ -617,7 +604,7 @@ export class PostgresDriver implements Driver {
/**
* Creates a query runner used to execute database queries.
*/
createQueryRunner(mode: ReplicationMode): QueryRunner {
createQueryRunner(mode: ReplicationMode): PostgresQueryRunner {
return new PostgresQueryRunner(this, mode)
}

View File

@ -17,16 +17,16 @@ 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 { BroadcasterResult } from "../../subscriber/BroadcasterResult"
import { InstanceChecker } from "../../util/InstanceChecker"
import { OrmUtils } from "../../util/OrmUtils"
import { VersionUtils } from "../../util/VersionUtils"
import { DriverUtils } from "../DriverUtils"
import { Query } from "../Query"
import { ColumnType } from "../types/ColumnTypes"
import { IsolationLevel } from "../types/IsolationLevel"
import { MetadataTableType } from "../types/MetadataTableType"
import { ReplicationMode } from "../types/ReplicationMode"
import { PostgresDriver } from "./PostgresDriver"
import { BroadcasterResult } from "../../subscriber/BroadcasterResult"
/**
* Runs queries on a single postgres database connection.
@ -3108,7 +3108,6 @@ export class PostgresQueryRunner
const isAnotherTransactionActive = this.isTransactionActive
if (!isAnotherTransactionActive) await this.startTransaction()
try {
const version = await this.getVersion()
// drop views
const selectViewDropsQuery =
`SELECT 'DROP VIEW IF EXISTS "' || schemaname || '"."' || viewname || '" CASCADE;' as "query" ` +
@ -3122,7 +3121,7 @@ export class PostgresQueryRunner
// drop materialized views
// Note: materialized views introduced in Postgres 9.3
if (VersionUtils.isGreaterOrEqual(version, "9.3")) {
if (DriverUtils.isReleaseVersionOrGreater(this.driver, "9.3")) {
const selectMatViewDropsQuery =
`SELECT 'DROP MATERIALIZED VIEW IF EXISTS "' || schemaname || '"."' || matviewname || '" CASCADE;' as "query" ` +
`FROM "pg_matviews" WHERE "schemaname" IN (${schemaNamesString})`
@ -3158,7 +3157,9 @@ export class PostgresQueryRunner
if (!isAnotherTransactionActive) {
await this.rollbackTransaction()
}
} catch (rollbackError) {}
} catch {
// no-op
}
throw error
}
}
@ -4155,9 +4156,11 @@ export class PostgresQueryRunner
/**
* Loads Postgres version.
*/
protected async getVersion(): Promise<string> {
const result = await this.query(`SELECT version()`)
return result[0]["version"].replace(/^PostgreSQL ([\d.]+) .*$/, "$1")
async getVersion(): Promise<string> {
const result: [{ version: string }] = await this.query(
`SELECT version()`,
)
return result[0].version.replace(/^PostgreSQL ([\d.]+) .*$/, "$1")
}
/**

View File

@ -71,6 +71,11 @@ export class SapDriver implements Driver {
*/
options: SapConnectionOptions
/**
* Version of SAP HANA. Requires a SQL query to the DB, so it is not always set
*/
version?: string
/**
* Database name used to perform all write queries.
*/
@ -295,19 +300,19 @@ export class SapDriver implements Driver {
// create the pool
this.master = this.client.createPool(dbParams, options)
if (!this.database || !this.schema) {
const queryRunner = await this.createQueryRunner("master")
const queryRunner = this.createQueryRunner("master")
if (!this.database) {
this.database = await queryRunner.getCurrentDatabase()
}
this.version = await queryRunner.getVersion()
if (!this.schema) {
this.schema = await queryRunner.getCurrentSchema()
}
await queryRunner.release()
if (!this.database) {
this.database = await queryRunner.getCurrentDatabase()
}
if (!this.schema) {
this.schema = await queryRunner.getCurrentSchema()
}
await queryRunner.release()
}
/**

View File

@ -386,10 +386,22 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner {
* Returns current database.
*/
async getCurrentDatabase(): Promise<string> {
const currentDBQuery = await this.query(
`SELECT "VALUE" AS "db_name" FROM "SYS"."M_SYSTEM_OVERVIEW" WHERE "SECTION" = 'System' and "NAME" = 'Instance ID'`,
const currentDBQuery: [{ dbName: string }] = await this.query(
`SELECT "DATABASE_NAME" AS "dbName" FROM "SYS"."M_DATABASE"`,
)
return currentDBQuery[0]["db_name"]
return currentDBQuery[0].dbName
}
/**
* Returns the database server version.
*/
async getVersion(): Promise<string> {
const currentDBQuery: [{ version: string }] = await this.query(
`SELECT "VERSION" AS "version" FROM "SYS"."M_DATABASE"`,
)
return currentDBQuery[0].version
}
/**
@ -404,10 +416,11 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner {
* Returns current schema.
*/
async getCurrentSchema(): Promise<string> {
const currentSchemaQuery = await this.query(
`SELECT CURRENT_SCHEMA AS "schema_name" FROM "SYS"."DUMMY"`,
const currentSchemaQuery: [{ schemaName: string }] = await this.query(
`SELECT CURRENT_SCHEMA AS "schemaName" FROM "SYS"."DUMMY"`,
)
return currentSchemaQuery[0]["schema_name"]
return currentSchemaQuery[0].schemaName
}
/**
@ -420,9 +433,10 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner {
parsedTableName.schema = await this.getCurrentSchema()
}
const sql = `SELECT * FROM "SYS"."TABLES" WHERE "SCHEMA_NAME" = '${parsedTableName.schema}' AND "TABLE_NAME" = '${parsedTableName.tableName}'`
const result = await this.query(sql)
return result.length ? true : false
const sql = `SELECT COUNT(*) as "hasTable" FROM "SYS"."TABLES" WHERE "SCHEMA_NAME" = '${parsedTableName.schema}' AND "TABLE_NAME" = '${parsedTableName.tableName}'`
const result: [{ hasTable: number }] = await this.query(sql)
return result[0].hasTable > 0
}
/**
@ -438,9 +452,10 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner {
parsedTableName.schema = await this.getCurrentSchema()
}
const sql = `SELECT * FROM "SYS"."TABLE_COLUMNS" WHERE "SCHEMA_NAME" = '${parsedTableName.schema}' AND "TABLE_NAME" = '${parsedTableName.tableName}' AND "COLUMN_NAME" = '${columnName}'`
const result = await this.query(sql)
return result.length ? true : false
const sql = `SELECT COUNT(*) as "hasColumn" FROM "SYS"."TABLE_COLUMNS" WHERE "SCHEMA_NAME" = '${parsedTableName.schema}' AND "TABLE_NAME" = '${parsedTableName.tableName}' AND "COLUMN_NAME" = '${columnName}'`
const result: [{ hasColumn: number }] = await this.query(sql)
return result[0].hasColumn > 0
}
/**

View File

@ -300,7 +300,7 @@ export class SqlServerDriver implements Driver {
}
if (!this.database || !this.searchSchema) {
const queryRunner = await this.createQueryRunner("master")
const queryRunner = this.createQueryRunner("master")
if (!this.database) {
this.database = await queryRunner.getCurrentDatabase()

View File

@ -1,22 +1,27 @@
export type Version = [number, number, number]
export class VersionUtils {
static isGreaterOrEqual(version: string, targetVersion: string): boolean {
static isGreaterOrEqual(
version: string | undefined,
targetVersion: string,
): boolean {
if (!version) {
return false
}
const v1 = parseVersion(version)
const v2 = parseVersion(targetVersion)
return (
v1[0] > v2[0] ||
(v1[0] === v2[0] && v1[1] > v2[1]) ||
(v1[0] === v2[0] && v1[1] === v2[1] && v1[2] >= v2[2])
)
for (let i = 0; i < v1.length && i < v2.length; i++) {
if (v1[i] > v2[i]) {
return true
} else if (v1[i] < v2[i]) {
return false
}
}
return true
}
}
function parseVersion(version: string = ""): Version {
const v: Version = [0, 0, 0]
version.split(".").forEach((value, i) => (v[i] = parseInt(value, 10)))
return v
function parseVersion(version: string): number[] {
return version.split(".").map((value) => parseInt(value, 10))
}

View File

@ -1,12 +1,12 @@
import { expect } from "chai"
import "reflect-metadata"
import { DataSource } from "../../../src/data-source/DataSource"
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases,
} from "../../utils/test-utils"
import { DataSource } from "../../../src/data-source/DataSource"
import { expect } from "chai"
import { VersionUtils } from "../../../src/util/VersionUtils"
describe("github issues > #4782 mariadb driver wants to recreate create/update date columns CURRENT_TIMESTAMP(6) === current_timestamp(6)", () => {
let connections: DataSource[]
@ -28,63 +28,4 @@ describe("github issues > #4782 mariadb driver wants to recreate create/update d
expect(sql1.upQueries).to.eql([])
}),
))
describe("VersionUtils", () => {
describe("isGreaterOrEqual", () => {
it("should return false when comparing invalid versions", () => {
expect(VersionUtils.isGreaterOrEqual("", "")).to.equal(false)
})
it("should return false when targetVersion is larger", () => {
expect(
VersionUtils.isGreaterOrEqual("1.2.3", "1.2.4"),
).to.equal(false)
expect(
VersionUtils.isGreaterOrEqual("1.2.3", "1.4.3"),
).to.equal(false)
expect(
VersionUtils.isGreaterOrEqual("1.2.3", "2.2.3"),
).to.equal(false)
expect(VersionUtils.isGreaterOrEqual("1.2", "1.3")).to.equal(
false,
)
expect(VersionUtils.isGreaterOrEqual("1", "2")).to.equal(false)
expect(
VersionUtils.isGreaterOrEqual(
undefined as unknown as string,
"0.0.1",
),
).to.equal(false)
})
it("should return true when targetVersion is smaller", () => {
expect(
VersionUtils.isGreaterOrEqual("1.2.3", "1.2.2"),
).to.equal(true)
expect(
VersionUtils.isGreaterOrEqual("1.2.3", "1.1.3"),
).to.equal(true)
expect(
VersionUtils.isGreaterOrEqual("1.2.3", "0.2.3"),
).to.equal(true)
expect(VersionUtils.isGreaterOrEqual("1.2", "1.2")).to.equal(
true,
)
expect(VersionUtils.isGreaterOrEqual("1", "1")).to.equal(true)
})
it("should work with mariadb-style versions", () => {
const dbVersion = "10.4.8-MariaDB-1:10.4.8+maria~bionic"
expect(
VersionUtils.isGreaterOrEqual("10.4.9", dbVersion),
).to.equal(true)
expect(
VersionUtils.isGreaterOrEqual("10.4.8", dbVersion),
).to.equal(true)
expect(
VersionUtils.isGreaterOrEqual("10.4.7", dbVersion),
).to.equal(false)
})
})
})
})

View File

@ -10,18 +10,20 @@ import { expect } from "chai"
import { VersionUtils } from "../../../src/util/VersionUtils"
describe('github issues > #7235 Use "INSERT...RETURNING" in MariaDB.', () => {
const runOnSpecificVersion = (version: string, fn: Function) => async () =>
Promise.all(
connections.map(async (connection) => {
const result = await connection.query(
`SELECT VERSION() AS \`version\``,
)
const dbVersion = result[0]["version"]
if (VersionUtils.isGreaterOrEqual(dbVersion, version)) {
await fn(connection)
}
}),
)
const runOnSpecificVersion =
(version: string, fn: (dataSource: DataSource) => Promise<void>) =>
async () =>
Promise.all(
connections.map(async (connection) => {
const result = await connection.query(
`SELECT VERSION() AS \`version\``,
)
const dbVersion = result[0]["version"]
if (VersionUtils.isGreaterOrEqual(dbVersion, version)) {
await fn(connection)
}
}),
)
let connections: DataSource[]

View File

@ -0,0 +1,77 @@
import { expect } from "chai"
import { VersionUtils } from "../../src/util/VersionUtils"
describe("VersionUtils", () => {
describe("isGreaterOrEqual", () => {
it("should return false when comparing invalid versions", () => {
expect(VersionUtils.isGreaterOrEqual("", "")).to.equal(false)
expect(VersionUtils.isGreaterOrEqual(undefined, "0.0.1")).to.equal(
false,
)
})
it("should return false when targetVersion is larger", () => {
expect(VersionUtils.isGreaterOrEqual("1.2.3", "1.2.4")).to.equal(
false,
)
expect(VersionUtils.isGreaterOrEqual("1.2.3", "1.4.3")).to.equal(
false,
)
expect(VersionUtils.isGreaterOrEqual("1.2.3", "2.2.3")).to.equal(
false,
)
expect(VersionUtils.isGreaterOrEqual("1.2", "1.3")).to.equal(false)
expect(VersionUtils.isGreaterOrEqual("1.2", "1.3.1")).to.equal(
false,
)
expect(VersionUtils.isGreaterOrEqual("1.2.3", "1.3")).to.equal(
false,
)
expect(VersionUtils.isGreaterOrEqual("1", "2")).to.equal(false)
expect(
VersionUtils.isGreaterOrEqual(
"2.00.040.00.20190729.1",
"2.00.076.00.1705400033",
),
).to.equal(false)
expect(
VersionUtils.isGreaterOrEqual(
"4.00.000.00.1732616693",
"4.00.000.00.1739875052",
),
).to.equal(false)
})
it("should return true when targetVersion is smaller", () => {
expect(VersionUtils.isGreaterOrEqual("1.2.3", "1.2.2")).to.equal(
true,
)
expect(VersionUtils.isGreaterOrEqual("1.2.3", "1.1.3")).to.equal(
true,
)
expect(VersionUtils.isGreaterOrEqual("1.2.3", "1.1.5")).to.equal(
true,
)
expect(VersionUtils.isGreaterOrEqual("1.2.3", "0.2.3")).to.equal(
true,
)
expect(VersionUtils.isGreaterOrEqual("1.2", "1.2")).to.equal(true)
expect(VersionUtils.isGreaterOrEqual("1.3", "1.2.3")).to.equal(true)
expect(VersionUtils.isGreaterOrEqual("1.2.3", "1.2")).to.equal(true)
expect(VersionUtils.isGreaterOrEqual("1", "1")).to.equal(true)
expect(
VersionUtils.isGreaterOrEqual(
"2.00.076.00.1705400033",
"2.00.040.00.20190729.1",
),
).to.equal(true)
expect(
VersionUtils.isGreaterOrEqual(
"4.00.000.00.1739875052",
"4.00.000.00.1732616693",
),
).to.equal(true)
})
})
})