From 2f0201a8bdf3b658237df285158b273145d74850 Mon Sep 17 00:00:00 2001 From: Ruslan Iushchenko Date: Thu, 5 Sep 2024 09:54:09 +0200 Subject: [PATCH] #482 Add unquoting capabilities to Sql Generators. --- .../co/absa/pramen/api/sql/SqlGenerator.scala | 7 +++ .../pramen/api/sql/SqlGeneratorBase.scala | 15 +++++ .../core/sql/SqlGeneratorMicrosoft.scala | 17 +++++ .../pramen/core/mocks/SqlGeneratorDummy.scala | 2 + .../tests/sql/SqlGeneratorLoaderSuite.scala | 62 +++++++++++++++++++ 5 files changed, 103 insertions(+) diff --git a/pramen/api/src/main/scala/za/co/absa/pramen/api/sql/SqlGenerator.scala b/pramen/api/src/main/scala/za/co/absa/pramen/api/sql/SqlGenerator.scala index 934aeaed5..947e331ed 100644 --- a/pramen/api/src/main/scala/za/co/absa/pramen/api/sql/SqlGenerator.scala +++ b/pramen/api/src/main/scala/za/co/absa/pramen/api/sql/SqlGenerator.scala @@ -85,6 +85,13 @@ trait SqlGenerator { */ def quote(identifier: String): String + /** + * Unquotes an identifier name with characters specific to SQL dialects. + * If the identifier is already not quoted, nothing will be done. + * It supports partially quoted identifiers. E.g. '"my_catalog".my table' will be quoted as 'my_catalog.my table'. + */ + def unquote(identifier: String): String + /** * Returns true if the SQL generator can only work if it has an active connection. * This can be for database engines that does not support "SELECT * FROM table" and require explicit list of columns. diff --git a/pramen/api/src/main/scala/za/co/absa/pramen/api/sql/SqlGeneratorBase.scala b/pramen/api/src/main/scala/za/co/absa/pramen/api/sql/SqlGeneratorBase.scala index 9a2d15a4b..15945a1c8 100644 --- a/pramen/api/src/main/scala/za/co/absa/pramen/api/sql/SqlGeneratorBase.scala +++ b/pramen/api/src/main/scala/za/co/absa/pramen/api/sql/SqlGeneratorBase.scala @@ -44,6 +44,16 @@ abstract class SqlGeneratorBase(sqlConfig: SqlConfig) extends SqlGenerator { } } + def unquoteSingleIdentifier(identifier: String): String = { + val (escapeBegin, escapeEnd) = beginEndEscapeChars + + if (identifier.startsWith(s"$escapeBegin") && identifier.endsWith(s"$escapeEnd") && identifier.length > 2) { + identifier.substring(1, identifier.length - 1) + } else { + identifier + } + } + override def getAliasExpression(expression: String, alias: String): String = { s"$expression AS ${escape(alias)}" } @@ -53,6 +63,11 @@ abstract class SqlGeneratorBase(sqlConfig: SqlConfig) extends SqlGenerator { splitComplexIdentifier(identifier).map(quoteSingleIdentifier).mkString(".") } + override final def unquote(identifier: String): String = { + validateIdentifier(identifier) + splitComplexIdentifier(identifier).map(unquoteSingleIdentifier).mkString(".") + } + override final def escape(identifier: String): String = { if (needsEscaping(sqlConfig.identifierQuotingPolicy, identifier)) { quote(identifier) diff --git a/pramen/core/src/main/scala/za/co/absa/pramen/core/sql/SqlGeneratorMicrosoft.scala b/pramen/core/src/main/scala/za/co/absa/pramen/core/sql/SqlGeneratorMicrosoft.scala index a5f285b7a..1d5064454 100644 --- a/pramen/core/src/main/scala/za/co/absa/pramen/core/sql/SqlGeneratorMicrosoft.scala +++ b/pramen/core/src/main/scala/za/co/absa/pramen/core/sql/SqlGeneratorMicrosoft.scala @@ -114,6 +114,11 @@ class SqlGeneratorMicrosoft(sqlConfig: SqlConfig) extends SqlGenerator { splitComplexIdentifier(identifier).map(quoteSingleIdentifier).mkString(".") } + override def unquote(identifier: String): String = { + validateIdentifier(identifier) + splitComplexIdentifier(identifier).map(unquoteSingleIdentifier).mkString(".") + } + override def escape(identifier: String): String = { if (needsEscaping(sqlConfig.identifierQuotingPolicy, identifier)) { quote(identifier) @@ -138,6 +143,18 @@ class SqlGeneratorMicrosoft(sqlConfig: SqlConfig) extends SqlGenerator { escape(sqlConfig.infoDateColumn) } + private def unquoteSingleIdentifier(identifier: String): String = { + val (escapeBegin, escapeEnd) = beginEndEscapeChars + + if (identifier.startsWith(s"$escapeBegin") && identifier.endsWith(s"$escapeEnd") && identifier.length > 2) { + identifier.substring(1, identifier.length - 1) + } else if (identifier.startsWith(s"$escapeChar2") && identifier.endsWith(s"$escapeChar2") && identifier.length > 2) { + identifier.substring(1, identifier.length - 1) + } else { + identifier + } + } + private def quoteSingleIdentifier(identifier: String): String = { val (escapeBegin, escapeEnd) = beginEndEscapeChars diff --git a/pramen/core/src/test/scala/za/co/absa/pramen/core/mocks/SqlGeneratorDummy.scala b/pramen/core/src/test/scala/za/co/absa/pramen/core/mocks/SqlGeneratorDummy.scala index e439686ab..2aaea5ad3 100644 --- a/pramen/core/src/test/scala/za/co/absa/pramen/core/mocks/SqlGeneratorDummy.scala +++ b/pramen/core/src/test/scala/za/co/absa/pramen/core/mocks/SqlGeneratorDummy.scala @@ -44,4 +44,6 @@ class SqlGeneratorDummy(sqlConfig: SqlConfig) extends SqlGenerator { override def escape(identifier: String): String = null override def quote(identifier: String): String = null + + override def unquote(identifier: String): String = null } diff --git a/pramen/core/src/test/scala/za/co/absa/pramen/core/tests/sql/SqlGeneratorLoaderSuite.scala b/pramen/core/src/test/scala/za/co/absa/pramen/core/tests/sql/SqlGeneratorLoaderSuite.scala index 586f2ed3b..c420a1423 100644 --- a/pramen/core/src/test/scala/za/co/absa/pramen/core/tests/sql/SqlGeneratorLoaderSuite.scala +++ b/pramen/core/src/test/scala/za/co/absa/pramen/core/tests/sql/SqlGeneratorLoaderSuite.scala @@ -185,6 +185,14 @@ class SqlGeneratorLoaderSuite extends AnyWordSpec with RelationalDbFixture { assert(actual == "\"System User\".\"Table Name\"") } } + + "unquote" should { + "quote each subfields separately" in { + val actual = gen.unquote("System User.\"Table Name\"") + + assert(actual == "System User.Table Name") + } + } } "Microsoft SQL generator" should { @@ -341,6 +349,20 @@ class SqlGeneratorLoaderSuite extends AnyWordSpec with RelationalDbFixture { } } + "unquote" should { + "quote each subfields separately using quotes" in { + val actual = genDate.unquote("System User.\"Table Name\"") + + assert(actual == "System User.Table Name") + } + + "quote each subfields separately using brackets" in { + val actual = genDate.unquote("[System User].[Table Name]") + + assert(actual == "System User.Table Name") + } + } + "splitComplexIdentifier" should { "throw on an empty identifier" in { assertThrows[IllegalArgumentException] { @@ -576,6 +598,14 @@ class SqlGeneratorLoaderSuite extends AnyWordSpec with RelationalDbFixture { } } + "unquote" should { + "quote each subfields separately" in { + val actual = gen.unquote("System User.\"Table Name\"") + + assert(actual == "System User.Table Name") + } + } + "splitComplexIdentifier" should { "throw on an empty identifier" in { assertThrows[IllegalArgumentException] { @@ -1025,6 +1055,14 @@ class SqlGeneratorLoaderSuite extends AnyWordSpec with RelationalDbFixture { assert(actual == "\"System User\".\"Table Name\"") } } + + "unquote" should { + "quote each subfields separately" in { + val actual = genDate.unquote("System User.\"Table Name\"") + + assert(actual == "System User.Table Name") + } + } } "MySQL SQL generator" should { @@ -1147,6 +1185,14 @@ class SqlGeneratorLoaderSuite extends AnyWordSpec with RelationalDbFixture { assert(actual == "`System User`.`Table Name`") } } + + "unquote" should { + "quote each subfields separately" in { + val actual = genDate.unquote("System User.`Table Name`") + + assert(actual == "System User.Table Name") + } + } } "DB2 SQL generator" should { @@ -1269,6 +1315,14 @@ class SqlGeneratorLoaderSuite extends AnyWordSpec with RelationalDbFixture { assert(actual == "\"System User\".\"Table Name\"") } } + + "unquote" should { + "quote each subfields separately" in { + val actual = genDate.unquote("System User.\"Table Name\"") + + assert(actual == "System User.Table Name") + } + } } "HSQL generator" should { @@ -1560,6 +1614,14 @@ class SqlGeneratorLoaderSuite extends AnyWordSpec with RelationalDbFixture { } } } + + "unquote" should { + "quote each subfields separately" in { + val actual = genDate.unquote("System User.\"Table Name\"") + + assert(actual == "System User.Table Name") + } + } } }