From ea16b19359972a5b8b87bd497a6ade63ae3d6d50 Mon Sep 17 00:00:00 2001 From: Ruslan Iushchenko Date: Thu, 27 Aug 2026 10:11:41 +0200 Subject: [PATCH 1/9] Add support for Spark 4 - needs spark-commons released. --- build.sbt | 4 +-- project/Dependencies.scala | 8 +++-- .../standardization/stages/TypeParser.scala | 5 ++- .../interpreter/stages/TypeParserSuite.scala | 33 ++++++++++--------- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/build.sbt b/build.sbt index d9b5374..ce0d32d 100644 --- a/build.sbt +++ b/build.sbt @@ -21,10 +21,10 @@ ThisBuild / name := "spark-data-standardization" ThisBuild / organization := "za.co.absa" lazy val scala212 = "2.12.20" -lazy val scala213 = "2.13.16" +lazy val scala213 = "2.13.17" ThisBuild / crossScalaVersions := Seq(scala212, scala213) -ThisBuild / scalaVersion := scala212 +ThisBuild / scalaVersion := scala213 ThisBuild / versionScheme := Some("early-semver") diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 7fb67b4..9c0c543 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -25,7 +25,11 @@ object Dependencies { private def sparkFastTestsVersion(scalaVersion: String): String = "1.1.0" - def getSparkVersion(scalaVersion: String): String = "3.5.7" + def getSparkVersion(scalaVersion: String): String = if (scalaVersion.startsWith("2.12")) + "3.5.7" + else + "4.1.2" + def dependencyList(scalaVersion: String): Seq[ModuleID] = { val sparkVersion = getSparkVersion(scalaVersion) @@ -33,7 +37,7 @@ object Dependencies { List( "org.apache.spark" %% "spark-core" % sparkVersion % Provided, "org.apache.spark" %% "spark-sql" % sparkVersion % Provided, - "za.co.absa" %% s"spark-commons-spark$sparkVersionUpToMinor" % "0.6.3" % Provided, + "za.co.absa" %% s"spark-commons-spark$sparkVersionUpToMinor" % "0.6.4-SNAPSHOT" % Provided, "za.co.absa" %% "spark-commons-test" % "0.6.3" % Test, "com.typesafe" % "config" % "1.4.2", "com.github.mrpowers" %% "spark-fast-tests" % sparkFastTestsVersion(scalaVersion) % Test, diff --git a/src/main/scala/za/co/absa/standardization/stages/TypeParser.scala b/src/main/scala/za/co/absa/standardization/stages/TypeParser.scala index 19802a6..3563139 100644 --- a/src/main/scala/za/co/absa/standardization/stages/TypeParser.scala +++ b/src/main/scala/za/co/absa/standardization/stages/TypeParser.scala @@ -25,7 +25,6 @@ import za.co.absa.spark.commons.implicits.ColumnImplicits.ColumnEnhancements import za.co.absa.spark.commons.implicits.StructFieldImplicits.StructFieldMetadataEnhancements import za.co.absa.spark.commons.implicits.StructTypeImplicits.StructTypeEnhancements import za.co.absa.spark.commons.utils.SchemaUtils -import za.co.absa.spark.hofs.transform import za.co.absa.standardization.config.StandardizationConfig import za.co.absa.standardization.implicits.StdColumnImplicits.StdColumnEnhancements import za.co.absa.standardization.schema.StdSchemaUtils.FieldWithSource @@ -214,9 +213,9 @@ object TypeParser { val finalErrs = when(nullErrCond, array(typedLit(StandardizationErrorMessage.stdNullErr(inputFullPathName)(stdConfig.errorCodes)))) .otherwise( - typedLit(flatten(transform(column, lambdaErrCols, lambdaVariableName))) + typedLit(flatten(transform(column, lambdaErrCols))) ) - val stdCols = transform(column, lambdaStdCols, lambdaVariableName) + val stdCols = transform(column, lambdaStdCols) logger.info(s"Finished standardization plan for Array $inputFullPathName") ParseOutput(stdCols as (fieldOutputName, metadata), finalErrs) } diff --git a/src/test/scala/za/co/absa/standardization/interpreter/stages/TypeParserSuite.scala b/src/test/scala/za/co/absa/standardization/interpreter/stages/TypeParserSuite.scala index 6d13274..e85b9f0 100644 --- a/src/test/scala/za/co/absa/standardization/interpreter/stages/TypeParserSuite.scala +++ b/src/test/scala/za/co/absa/standardization/interpreter/stages/TypeParserSuite.scala @@ -19,10 +19,11 @@ package za.co.absa.standardization.interpreter.stages import org.apache.spark.sql.types._ import org.scalatest.funsuite.AnyFunSuite import za.co.absa.spark.commons.test.SparkTestBase +import za.co.absa.spark.commons.utils.ColUtils import za.co.absa.standardization.RecordIdGeneration.IdType.NoId -import za.co.absa.standardization.config.{BasicMetadataColumnsConfig, BasicStandardizationConfig, StandardizationConfig} +import za.co.absa.standardization.config.{BasicMetadataColumnsConfig, BasicStandardizationConfig} import za.co.absa.standardization.stages.TypeParser -import za.co.absa.standardization.types.{TypeDefaults, CommonTypeDefaults} +import za.co.absa.standardization.types.{CommonTypeDefaults, TypeDefaults} import za.co.absa.standardization.udf.UDFLibrary class TypeParserSuite extends AnyFunSuite with SparkTestBase { @@ -44,21 +45,21 @@ class TypeParserSuite extends AnyFunSuite with SparkTestBase { val schema = StructType(Array(structFieldNoMetadata, structFieldWithMetadataNotSourceColumn, structFieldWithMetadataSourceColumn)) //Just Testing field name override val parseOutputStructFieldNoMetadata = TypeParser.standardize(structFieldNoMetadata, "path", schema, stdConfig) - assertResult(true)(parseOutputStructFieldNoMetadata.stdCol.expr.toString().contains("path.a")) - assertResult(false)(parseOutputStructFieldNoMetadata.stdCol.expr.toString().replaceAll("path.a", "").contains("path")) - assertResult(true)(parseOutputStructFieldNoMetadata.errors.expr.toString().contains("path.a")) - assertResult(false)(parseOutputStructFieldNoMetadata.errors.expr.toString().replaceAll("path.a", "").contains("path")) + assertResult(true)(ColUtils.col2Expr(parseOutputStructFieldNoMetadata.stdCol).toString().contains("path.a")) + assertResult(false)(ColUtils.col2Expr(parseOutputStructFieldNoMetadata.stdCol).toString().replaceAll("path.a", "").contains("path")) + assertResult(true)(ColUtils.col2Expr(parseOutputStructFieldNoMetadata.errors).toString().contains("path.a")) + assertResult(false)(ColUtils.col2Expr(parseOutputStructFieldNoMetadata.errors).toString().replaceAll("path.a", "").contains("path")) val parseOutputStructFieldWithMetadataNotSourceColumn = TypeParser.standardize(structFieldWithMetadataNotSourceColumn, "path", schema, stdConfig) - assertResult(true)(parseOutputStructFieldWithMetadataNotSourceColumn.stdCol.expr.toString().contains("path.b")) - assertResult(false)(parseOutputStructFieldWithMetadataNotSourceColumn.stdCol.expr.toString().replaceAll("path.b", "").contains("path")) - assertResult(true)(parseOutputStructFieldWithMetadataNotSourceColumn.errors.expr.toString().contains("path.b")) - assertResult(false)(parseOutputStructFieldWithMetadataNotSourceColumn.errors.expr.toString().replaceAll("path.b", "").contains("path")) + assertResult(true)(ColUtils.col2Expr(parseOutputStructFieldWithMetadataNotSourceColumn.stdCol).toString().contains("path.b")) + assertResult(false)(ColUtils.col2Expr(parseOutputStructFieldWithMetadataNotSourceColumn.stdCol).toString().replaceAll("path.b", "").contains("path")) + assertResult(true)(ColUtils.col2Expr(parseOutputStructFieldWithMetadataNotSourceColumn.errors).toString().contains("path.b")) + assertResult(false)(ColUtils.col2Expr(parseOutputStructFieldWithMetadataNotSourceColumn.errors).toString().replaceAll("path.b", "").contains("path")) val parseOutputStructFieldWithMetadataSourceColumn = TypeParser.standardize(structFieldWithMetadataSourceColumn, "path", schema, stdConfig) - assertResult(false)(parseOutputStructFieldWithMetadataSourceColumn.stdCol.expr.toString().contains("path.c")) - assertResult(true)(parseOutputStructFieldWithMetadataSourceColumn.stdCol.expr.toString().contains("path.override_c")) - assertResult(false)(parseOutputStructFieldWithMetadataSourceColumn.stdCol.expr.toString().replaceAll("path.override_c", "").contains("path")) - assertResult(false)(parseOutputStructFieldWithMetadataSourceColumn.errors.expr.toString().contains("path.c")) - assertResult(true)(parseOutputStructFieldWithMetadataSourceColumn.errors.expr.toString().contains("path.override_c")) - assertResult(false)(parseOutputStructFieldWithMetadataSourceColumn.errors.expr.toString().replaceAll("path.override_c", "").contains("path")) + assertResult(false)(ColUtils.col2Expr(parseOutputStructFieldWithMetadataSourceColumn.stdCol).toString().contains("path.c")) + assertResult(true)(ColUtils.col2Expr(parseOutputStructFieldWithMetadataSourceColumn.stdCol).toString().contains("path.override_c")) + assertResult(false)(ColUtils.col2Expr(parseOutputStructFieldWithMetadataSourceColumn.stdCol).toString().replaceAll("path.override_c", "").contains("path")) + assertResult(false)(ColUtils.col2Expr(parseOutputStructFieldWithMetadataSourceColumn.errors).toString().contains("path.c")) + assertResult(true)(ColUtils.col2Expr(parseOutputStructFieldWithMetadataSourceColumn.errors).toString().contains("path.override_c")) + assertResult(false)(ColUtils.col2Expr(parseOutputStructFieldWithMetadataSourceColumn.errors).toString().replaceAll("path.override_c", "").contains("path")) } } From 5ca16d5f2b8c83191134a22994e6c8efa8eb7560 Mon Sep 17 00:00:00 2001 From: MatloaItumeleng Date: Mon, 21 Sep 2026 12:30:14 +0200 Subject: [PATCH 2/9] Spark4 upgrade, added ANSI-safe casting and newer JDK test assertion --- .github/workflows/build.yml | 9 +-- README.md | 12 +++- project/Dependencies.scala | 6 +- .../standardization/Standardization.scala | 61 +++++++++++++------ ...egralParser_RadixIntegralParserSuite.scala | 6 +- 5 files changed, 63 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a98663a..78f5f04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: strategy: fail-fast: false matrix: - scala: [2.12.18, 2.13.11] + scala: [2.12.20, 2.13.17] name: Scala ${{matrix.scala}} steps: - name: Checkout code @@ -38,10 +38,11 @@ jobs: - uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d - - name: Setup Scala - uses: olafurpg/setup-scala@32ffa16635ff8f19cc21ea253a987f0fdf29844c + - name: Setup JDK + uses: actions/setup-java@v4 with: - java-version: "adopt@1.11" + distribution: temurin + java-version: 17 - name: Build and run tests run: sbt ++${{matrix.scala}} test doc diff --git a/README.md b/README.md index 976891d..5ca406c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) [![Release](https://github.com/AbsaOSS/spark-data-standardization/actions/workflows/release.yml/badge.svg)](https://github.com/AbsaOSS/spark-data-standardization/actions/workflows/release.yml) ![Java 11](https://img.shields.io/badge/Java_11-ED8B00?style=flat&logo=openjdk&logoColor=black) +![Java 17](https://img.shields.io/badge/Java_17-ED8B00?style=flat&logo=openjdk&logoColor=black) - Dataframe in - Standardized Dataframe out @@ -16,7 +17,7 @@ The library needs following dependencies to be included in your project ```sbt "org.apache.spark" %% "spark-core" % SPARK_VERSION, "org.apache.spark" %% "spark-sql" % SPARK_VERSION, -"za.co.absa" %% s"spark-commons-spark${SPARK_MAJOR}.${SPARK_MINOR}" % "0.6.3", +"za.co.absa" %% s"spark-commons-spark${SPARK_MAJOR}.${SPARK_MINOR}" % "1.0.0", ``` ### Usage in SBT: @@ -49,7 +50,14 @@ The library needs following dependencies to be included in your project Spark and Scala compatibility >| | Scala 2.12 | Scala 2.13 | >|---|---|---| ->|Spark| 3.5.x | 3.5.x | +>|Spark| 3.5.x | 4.1.x | + +Java compatibility + + +>| | Scala 2.12 (Spark 3.5.x) | Scala 2.13 (Spark 4.1.x) | +>|---|---|---| +>|Java| 8 / 11 / 17 | 17 | ## How to Release diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 9c0c543..cd3d63d 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -37,11 +37,11 @@ object Dependencies { List( "org.apache.spark" %% "spark-core" % sparkVersion % Provided, "org.apache.spark" %% "spark-sql" % sparkVersion % Provided, - "za.co.absa" %% s"spark-commons-spark$sparkVersionUpToMinor" % "0.6.4-SNAPSHOT" % Provided, - "za.co.absa" %% "spark-commons-test" % "0.6.3" % Test, + "za.co.absa" %% s"spark-commons-spark$sparkVersionUpToMinor" % "1.0.0" % Provided, + "za.co.absa" %% "spark-commons-test" % "1.0.0" % Test, "com.typesafe" % "config" % "1.4.2", "com.github.mrpowers" %% "spark-fast-tests" % sparkFastTestsVersion(scalaVersion) % Test, - "org.scalatest" %% "scalatest" % "3.2.15" % Test + "org.scalatest" %% "scalatest" % "3.2.2" % Test ) } } diff --git a/src/main/scala/za/co/absa/standardization/Standardization.scala b/src/main/scala/za/co/absa/standardization/Standardization.scala index fb8a70e..d59524c 100644 --- a/src/main/scala/za/co/absa/standardization/Standardization.scala +++ b/src/main/scala/za/co/absa/standardization/Standardization.scala @@ -39,27 +39,50 @@ object Standardization { udfLib.register(sparkSession) implicit val defaults: TypeDefaults = standardizationConfig.typeDefaults - logger.info(s"Step 1: Schema validation") - validateSchemaAgainstSelfInconsistencies(schema) - - logger.info(s"Step 2: Standardization") - val std = standardizeDataset(df, schema, standardizationConfig) - - logger.info(s"Step 3: Clean the final error column") - val cleanedStd = cleanTheFinalErrorColumn(std) - - val idedStd = if (cleanedStd.schema.fieldExists(standardizationConfig.metadataColumns.recordId)) { - cleanedStd // no new id regeneration - } else { - RecordIdGeneration.addRecordIdColumnByStrategy( - cleanedStd, - standardizationConfig.metadataColumns.recordId, - standardizationConfig.metadataColumns.recordIdStrategy - ) + withNonAnsiCasts(sparkSession) { + logger.info(s"Step 1: Schema validation") + validateSchemaAgainstSelfInconsistencies(schema) + + logger.info(s"Step 2: Standardization") + val std = standardizeDataset(df, schema, standardizationConfig) + + logger.info(s"Step 3: Clean the final error column") + val cleanedStd = cleanTheFinalErrorColumn(std) + + val idedStd = if (cleanedStd.schema.fieldExists(standardizationConfig.metadataColumns.recordId)) { + cleanedStd // no new id regeneration + } else { + RecordIdGeneration.addRecordIdColumnByStrategy( + cleanedStd, + standardizationConfig.metadataColumns.recordId, + standardizationConfig.metadataColumns.recordIdStrategy + ) + } + + logger.info(s"Standardization process finished, returning to the application...") + idedStd } + } - logger.info(s"Standardization process finished, returning to the application...") - idedStd + /** + * Standardization treats cast failures (invalid values, overflows and unparseable dates) as error + * records relying on Spark returning `null` from a failed casts. Spark 4 upgrade enables ANSI SQL mode + * by default , this throws errors and aborts the job. This helper temporarily disables ANSI mode during + * plan construction and analysis to preserve the same behavior (null-on-failure) for both Spark 3.5 and + * Spark 4. The original value is restored after. + */ + private def withNonAnsiCasts[T](spark: SparkSession)(body: => T): T = { + val ansiConfKey = "spark.sql.ansi.enabled" + val previous = scala.util.Try(spark.conf.get(ansiConfKey)).toOption + spark.conf.set(ansiConfKey, "false") + try { + body + } finally { + previous match { + case Some(value) => spark.conf.set(ansiConfKey, value) + case None => spark.conf.unset(ansiConfKey) + } + } } diff --git a/src/test/scala/za/co/absa/standardization/types/parsers/IntegralParser_RadixIntegralParserSuite.scala b/src/test/scala/za/co/absa/standardization/types/parsers/IntegralParser_RadixIntegralParserSuite.scala index 0c94bdd..200a0e1 100644 --- a/src/test/scala/za/co/absa/standardization/types/parsers/IntegralParser_RadixIntegralParserSuite.scala +++ b/src/test/scala/za/co/absa/standardization/types/parsers/IntegralParser_RadixIntegralParserSuite.scala @@ -80,7 +80,7 @@ class IntegralParser_RadixIntegralParserSuite extends AnyFunSuite { val wrong = "g" val fail = parser.parse(wrong).failed.get assert(fail.isInstanceOf[NumberFormatException]) - assert(fail.getMessage == """For input string: "g"""") + assert(fail.getMessage.startsWith("""For input string: "g"""")) } test("base 2 parsing succeeds") { @@ -103,7 +103,7 @@ class IntegralParser_RadixIntegralParserSuite extends AnyFunSuite { val wrong = "3" val fail = parser.parse(wrong).failed.get assert(fail.isInstanceOf[NumberFormatException]) - assert(fail.getMessage == """For input string: """" + wrong + """"""") + assert(fail.getMessage.startsWith("""For input string: """" + wrong + """"""")) } @@ -127,7 +127,7 @@ class IntegralParser_RadixIntegralParserSuite extends AnyFunSuite { val wrong = "__" val fail = parser.parse(wrong).failed.get assert(fail.isInstanceOf[NumberFormatException]) - assert(fail.getMessage == """For input string: """" + wrong + """"""") + assert(fail.getMessage.startsWith("""For input string: """" + wrong + """"""")) } test("string base inputs") { From faea6daf78b87d5c69e1e3b951b0d408bd4f5a15 Mon Sep 17 00:00:00 2001 From: MatloaItumeleng Date: Mon, 21 Sep 2026 13:33:11 +0200 Subject: [PATCH 3/9] fix: runner failure --- .github/workflows/build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 78f5f04..b30b66f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,13 +36,11 @@ jobs: with: persist-credentials: false - - uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d - - name: Setup JDK uses: actions/setup-java@v4 with: - distribution: temurin - java-version: 17 + java-version: "17" + distribution: "temurin" - name: Build and run tests run: sbt ++${{matrix.scala}} test doc From 287ab83eb06f02ce7e2cb2418539a4beeb010251 Mon Sep 17 00:00:00 2001 From: MatloaItumeleng Date: Mon, 21 Sep 2026 14:16:16 +0200 Subject: [PATCH 4/9] using supported image version for sbt --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b30b66f..19c3215 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: From d1c9c14e765c7cbeb20221948e3bfcb1453aef16 Mon Sep 17 00:00:00 2001 From: MatloaItumeleng Date: Mon, 21 Sep 2026 14:45:36 +0200 Subject: [PATCH 5/9] fix: Java 17 spark access failures --- build.sbt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/build.sbt b/build.sbt index ce0d32d..41be870 100644 --- a/build.sbt +++ b/build.sbt @@ -45,6 +45,27 @@ Test / parallelExecution := false Test / logBuffered := false Test / fork := true +// Providing Spark required access to encapsulated JDK internals when running on Java 17+. +// to avoid failures IllegalAccessError (e.g. sun.nio.ch.DirectBuffer) & IllegalAccessException +// (e.g. sun.util.calendar.ZoneInfo) , They don't affect Java 11. +Test / javaOptions ++= Seq( + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED", + "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED", + "--add-opens=java.base/java.io=ALL-UNNAMED", + "--add-opens=java.base/java.net=ALL-UNNAMED", + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + "--add-opens=java.base/java.util.concurrent=ALL-UNNAMED", + "--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED", + "--add-opens=java.base/jdk.internal.ref=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED", + "--add-opens=java.base/sun.nio.cs=ALL-UNNAMED", + "--add-opens=java.base/sun.security.action=ALL-UNNAMED", + "--add-opens=java.base/sun.util.calendar=ALL-UNNAMED", + "--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED" +) + // Only apply scalafmt to files that differ from master (i.e. files changed in the feature branch or so; n/a on Windows) lazy val fmtFilterExpression: String = System.getProperty("os.name").toLowerCase match { case win if win.contains("win") => "" From 0bbccda387a39eb4ef854842aa2eef45b29e453e Mon Sep 17 00:00:00 2001 From: MatloaItumeleng Date: Mon, 21 Sep 2026 17:18:35 +0200 Subject: [PATCH 6/9] fix: added normalizer for 2.13 spark4 error expression difference --- .../stages/TypeParserSuiteTemplate.scala | 97 ++++++++++++++++++- 1 file changed, 93 insertions(+), 4 deletions(-) diff --git a/src/test/scala/za/co/absa/standardization/interpreter/stages/TypeParserSuiteTemplate.scala b/src/test/scala/za/co/absa/standardization/interpreter/stages/TypeParserSuiteTemplate.scala index c9b5bd2..8336c0f 100644 --- a/src/test/scala/za/co/absa/standardization/interpreter/stages/TypeParserSuiteTemplate.scala +++ b/src/test/scala/za/co/absa/standardization/interpreter/stages/TypeParserSuiteTemplate.scala @@ -302,14 +302,103 @@ trait TypeParserSuiteTemplate extends AnyFunSuite with SparkTestBase { } private def doAssert(expectedExpression: String, actualExpression: String, method: String): Unit = { - if (actualExpression != expectedExpression) { + val (expected, actual) = + if (SPARK_VERSION.startsWith("4.")) (normalizeExpr(expectedExpression), normalizeExpr(actualExpression)) + else (expectedExpression, actualExpression) + + if (actual != expected) { // the expressions tend to be rather long, the assert most often cuts the beginning and/or end of the string // showing just the vicinity of the difference, so we log the output of the whole strings log.error(s"Method: $method") - log.error(s"Expected: $expectedExpression") - log.error(s"Actual : $actualExpression") - assert(actualExpression == expectedExpression) + log.error(s"Expected: $expected") + log.error(s"Actual : $actual") + assert(actual == expected) + } + } + + /** + * Spark 4 changed how Column.toString renders expressions compared to Spark 3 and older versions. + * This normalizer converts both expected and actual expressions to the same form , it is only applied for Spark 4. + */ + private def normalizeExpr(expr: String): String = { + transformPrefixOps(expr) + .replace("isNaN(", "isnan(") + .replace("List()", "[]") + .replace("ARRAY()", "[]") + .replace("`", "") + .replace("'", "") + .replaceAll("(DATE|TIMESTAMP) (?=\\d{4})", "") + .replaceAll("(\\d\\d:\\d\\d:\\d\\d)\\.0", "$1") + .replaceAll("(\\d)L", "$1") + } + + private val prefixOps = List("isNotNull", "isNull", "and", "or", "in", ">=", "<=", "=", ">", "<", "/", "%") + + private def transformPrefixOps(s: String): String = { + val sb = new StringBuilder + var i = 0 + val n = s.length + while (i < n) { + val c = s.charAt(i) + val boundary = i == 0 || { val p = s.charAt(i - 1); p == '(' || p == ',' || p == ' ' } + if (c == '!' && i + 1 < n && s.charAt(i + 1) == '(') { + val close = matchParen(s, i + 1) + val inner = s.substring(i + 2, close) + sb.append("(NOT ").append(transformPrefixOps(inner)).append(")") + i = close + 1 + } else if (boundary && matchOpAt(s, i).isDefined) { + val op = matchOpAt(s, i).get + val openIdx = i + op.length + val close = matchParen(s, openIdx) + val args = splitArgs(s.substring(openIdx + 1, close)).map(a => transformPrefixOps(a.trim)) + sb.append(renderInfix(op, args)) + i = close + 1 + } else { + sb.append(c) + i += 1 + } + } + sb.toString + } + + private def matchOpAt(s: String, i: Int): Option[String] = + prefixOps.find(op => s.startsWith(op + "(", i)) + + private def matchParen(s: String, openIdx: Int): Int = { + var depth = 0 + var i = openIdx + while (i < s.length) { + s.charAt(i) match { + case '(' => depth += 1 + case ')' => depth -= 1; if (depth == 0) return i + case _ => + } + i += 1 + } + throw new IllegalArgumentException(s"Unbalanced parentheses in expression: $s") + } + + private def splitArgs(inner: String): List[String] = { + val args = scala.collection.mutable.ListBuffer.empty[String] + val sb = new StringBuilder + var depth = 0 + inner.foreach { + case '(' => depth += 1; sb.append('(') + case ')' => depth -= 1; sb.append(')') + case ',' if depth == 0 => args += sb.toString; sb.setLength(0) + case ch => sb.append(ch) } + if (sb.nonEmpty || args.nonEmpty) args += sb.toString + args.toList + } + + private def renderInfix(op: String, args: List[String]): String = op match { + case "isNull" => s"(${args.head} IS NULL)" + case "isNotNull" => s"(${args.head} IS NOT NULL)" + case "and" => s"(${args.head} AND ${args(1)})" + case "or" => s"(${args.head} OR ${args(1)})" + case "in" => s"(${args.head} IN (${args.tail.mkString(", ")}))" + case symbol => s"(${args.head} $symbol ${args(1)})" } } From a3a0e2eedf81fe16c6c0e1b4ab40c7427775e504 Mon Sep 17 00:00:00 2001 From: MatloaItumeleng Date: Tue, 13 Oct 2026 11:27:55 +0200 Subject: [PATCH 7/9] review comments : full cross-build driven by versionAxis , workflow updates , Scala doc added and ADR introduced for workaround --- .github/workflows/build.yml | 19 ++- .github/workflows/format_check.yml | 9 +- .github/workflows/jacoco_check.yml | 36 +++-- .github/workflows/licence_check.yml | 9 +- .github/workflows/release.yml | 10 +- README.md | 64 +++++++-- build.sbt | 86 +++++++++--- project/Dependencies.scala | 16 +-- project/SparkVersionAxis.scala | 53 +++++++ project/plugins.sbt | 3 + .../standardization/Standardization.scala | 47 +++++-- .../002-ansi-handling/ansi-handling.drawio | 85 ++++++++++++ .../adr/002-ansi-handling/ansi-handling.md | 130 ++++++++++++++++++ .../adr/002-ansi-handling/ansi-handling.png | Bin 0 -> 137590 bytes .../StandardizationAnsiModeSuite.scala | 122 ++++++++++++++++ .../stages/TypeParserSuiteTemplate.scala | 35 ++++- 16 files changed, 641 insertions(+), 83 deletions(-) create mode 100644 project/SparkVersionAxis.scala create mode 100644 src/main/scala/za/co/absa/standardization/adr/002-ansi-handling/ansi-handling.drawio create mode 100644 src/main/scala/za/co/absa/standardization/adr/002-ansi-handling/ansi-handling.md create mode 100644 src/main/scala/za/co/absa/standardization/adr/002-ansi-handling/ansi-handling.png create mode 100644 src/test/scala/za/co/absa/standardization/StandardizationAnsiModeSuite.scala diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 19c3215..45dceba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,8 +28,14 @@ jobs: strategy: fail-fast: false matrix: - scala: [2.12.20, 2.13.17] - name: Scala ${{matrix.scala}} + # sbt per project (Scala, Spark) row of the projectMatrix in build.sbt, Spark 4 with Java 17 + include: + - { scala: 2.12.20, spark: 3.5.7, java: '11', project: standardization2_12Spark35 } + - { scala: 2.12.20, spark: 3.5.7, java: '17', project: standardization2_12Spark35 } + - { scala: 2.13.17, spark: 3.5.7, java: '11', project: standardizationSpark35 } + - { scala: 2.13.17, spark: 3.5.7, java: '17', project: standardizationSpark35 } + - { scala: 2.13.17, spark: 4.1.2, java: '17', project: standardizationSpark41 } + name: Scala ${{matrix.scala}}, Spark ${{matrix.spark}}, Java ${{matrix.java}} steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -37,10 +43,11 @@ jobs: persist-credentials: false - name: Setup JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: - java-version: "17" - distribution: "temurin" + java-version: ${{ matrix.java }} + distribution: temurin + cache: "sbt" - name: Build and run tests - run: sbt ++${{matrix.scala}} test doc + run: sbt ${{matrix.project}}/test ${{matrix.project}}/doc diff --git a/.github/workflows/format_check.yml b/.github/workflows/format_check.yml index 6ea1db9..a3ba658 100644 --- a/.github/workflows/format_check.yml +++ b/.github/workflows/format_check.yml @@ -22,7 +22,7 @@ on: jobs: scalafmt: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 name: Scalafmt Check steps: - name: Checkout code @@ -34,10 +34,11 @@ jobs: - uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d - - name: Setup Scala - uses: olafurpg/setup-scala@32ffa16635ff8f19cc21ea253a987f0fdf29844c + - name: Setup JDK + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: - java-version: "adopt@1.11" + distribution: temurin + java-version: '17' - name: Run scalafmt And Print Diff continue-on-error: true diff --git a/.github/workflows/jacoco_check.yml b/.github/workflows/jacoco_check.yml index b0070e5..fc1211a 100644 --- a/.github/workflows/jacoco_check.yml +++ b/.github/workflows/jacoco_check.yml @@ -23,21 +23,34 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: + fail-fast: false matrix: include: - # The project supports Scala 2.12, 2.13 - # The CI runs all tests suites for all supported Scala versions at build.yml - # The codebase for all Scala versions is the same, so the coverage is calculated only once - # Scala 2.12 is chosen since it is supported by the most wide range of Spark versions and - # vendor distributions. + # The project is built for (Scala 2.12, Spark 3.5), (Scala 2.13, Spark 3.5) and (Scala 2.13, Spark 4.1) + # The CI runs all tests suites for all of them at build.yml + # The codebase is shared but the runtime paths differ per Spark major version ( ANSI mode is on by default + # in Spark 4) so coverage is checked once per Spark major version: + # - Scala 2.12 / Spark 3.5: supported by the wide range of Spark versions and vendor distributions + # - Scala 2.13 / Spark 4.1: the only Spark 4 row - scala: 2.12.20 scalaShort: "2.12" + spark: 3.5.7 + project: standardization2_12Spark35 overall: 80.0 changed: 80.0 per-changed-file: 60.0 + - scala: 2.13.17 + scalaShort: "2.13" + spark: 4.1.2 + project: standardizationSpark41 + overall: 75.0 + changed: 80.0 + per-changed-file: 60.0 + name: JaCoCo - Scala ${{matrix.scala}}, Spark ${{matrix.spark}} + steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -46,10 +59,11 @@ jobs: - uses: coursier/cache-action@95e5b1029b6b86e7bac033ee44a0697d8a527d2d - - name: Setup Scala - uses: olafurpg/setup-scala@32ffa16635ff8f19cc21ea253a987f0fdf29844c + - name: Setup JDK + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: - java-version: "adopt@1.11" + distribution: temurin + java-version: '17' - name: Setup Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 @@ -57,7 +71,7 @@ jobs: python-version: "3.14" - name: Build and run tests - run: sbt ++${{matrix.scala}} jacoco + run: sbt "project ${{matrix.project}}" jacoco - name: Add JaCoCo Report in PR comments id: jacoco @@ -68,7 +82,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} global-thresholds: '${{ matrix.overall }}*${{ matrix.changed }}' report-thresholds-default: '${{ matrix.overall }}*${{ matrix.changed }}*${{ matrix.per-changed-file }}' - title: JaCoCo code coverage report - scala ${{ matrix.scala }} + title: JaCoCo code coverage report - scala ${{ matrix.scala }}, spark ${{ matrix.spark }} update-comment: true - name: Get the Coverage info diff --git a/.github/workflows/licence_check.yml b/.github/workflows/licence_check.yml index afbc4d3..d1fe69a 100644 --- a/.github/workflows/licence_check.yml +++ b/.github/workflows/licence_check.yml @@ -24,17 +24,18 @@ on: jobs: license-test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: persist-credentials: false - - name: Setup Scala - uses: olafurpg/setup-scala@32ffa16635ff8f19cc21ea253a987f0fdf29844c + - name: Setup JDK + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: - java-version: "adopt@1.11" + distribution: temurin + java-version: '17' # note, that task "headerCheck" defaults to just "compile:headerCheck" - see https://github.com/sbt/sbt-header/issues/14 - name: SBT src licence header check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1718f16..9058a5a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ on: jobs: publish: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -28,10 +28,12 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Setup Scala - uses: olafurpg/setup-scala@32ffa16635ff8f19cc21ea253a987f0fdf29844c + - name: Setup JDK + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: - java-version: "adopt@1.11" + distribution: temurin + java-version: '17' + - run: sbt ci-release env: diff --git a/README.md b/README.md index 5ca406c..0b1b9ea 100644 --- a/README.md +++ b/README.md @@ -21,53 +21,95 @@ The library needs following dependencies to be included in your project ``` ### Usage in SBT: + +The artifact name carries the Spark `major.minor` version it is built against, pick the one matching your Spark: + ```sbt -"za.co.absa" %% "spark-data-standardization" % VERSION +"za.co.absa" %% s"spark-data-standardization-spark${SPARK_MAJOR}.${SPARK_MINOR}" % VERSION ``` +e.g. `"za.co.absa" %% "spark-data-standardization-spark4.1" % VERSION` for Spark 4.1.x on Scala 2.13. ### Usage in Maven -### Scala 2.12 [![Maven Central](https://maven-badges.herokuapp.com/maven-central/za.co.absa/spark-data-standardization_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/za.co.absa/spark-data-standardization_2.12) +### Spark 3.5, Scala 2.12 [![Maven Central](https://maven-badges.herokuapp.com/maven-central/za.co.absa/spark-data-standardization-spark3.5_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/za.co.absa/spark-data-standardization-spark3.5_2.12) + +```xml + + za.co.absa + spark-data-standardization-spark3.5_2.12 + ${latest_version} + +``` + +### Spark 3.5, Scala 2.13 [![Maven Central](https://maven-badges.herokuapp.com/maven-central/za.co.absa/spark-data-standardization-spark3.5_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/za.co.absa/spark-data-standardization-spark3.5_2.13) ```xml za.co.absa - spark-data-standardization_2.12 + spark-data-standardization-spark3.5_2.13 ${latest_version} ``` -### Scala 2.13 [![Maven Central](https://maven-badges.herokuapp.com/maven-central/za.co.absa/spark-data-standardization_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/za.co.absa/spark-data-standardization_2.13) +### Spark 4.1, Scala 2.13 [![Maven Central](https://maven-badges.herokuapp.com/maven-central/za.co.absa/spark-data-standardization-spark4.1_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/za.co.absa/spark-data-standardization-spark4.1_2.13) ```xml za.co.absa - spark-data-standardization_2.13 + spark-data-standardization-spark4.1_2.13 ${latest_version} ``` +> **Upgrading from versions without the Spark suffix:** the previous `spark-data-standardization_2.12` (Spark 3.5) +> and `spark-data-standardization_2.13` artifacts are replaced by the `-spark.` artifacts above, +> so update the artifact id in your dependency coordinates. + Spark and Scala compatibility >| | Scala 2.12 | Scala 2.13 | >|---|---|---| ->|Spark| 3.5.x | 4.1.x | +>|Spark| 3.5.x | 3.5.x / 4.1.x | Java compatibility +>| | Scala 2.12 (Spark 3.5.x) | Scala 2.13 (Spark 3.5.x) | Scala 2.13 (Spark 4.1.x) | +>|---|---|---|---| +>|Java| 11 / 17 | 11 / 17 | 17 | ->| | Scala 2.12 (Spark 3.5.x) | Scala 2.13 (Spark 4.1.x) | ->|---|---|---| ->|Java| 8 / 11 / 17 | 17 | +### ANSI mode +Standardization reports values that can't be cast (unparseable, overflowing, invalid dates, ...) as `null` plus an +entry in the error column, never as a failing query. On Spark 4, where `spark.sql.ansi.enabled` defaults to `true`, +`standardize` therefore turns ANSI mode off on the session while the standardized DataFrame is being built (Spark +fixes a cast's evaluation mode when the plan is analysed, this is enough also for actions run later) and restores +the original value afterwards, even with concurrent `standardize` calls on the same session, see [ANSI handling](src/main/scala/za/co/absa/standardization/adr/002-ansi-handling/ansi-handling.md). ## How to Release Please see [this file](RELEASE.md) for more details. +## How to build and test + +The build is an [sbt-projectmatrix](https://github.com/sbt/sbt-projectmatrix), with one sbt project per supported +(Scala, Spark) combination: + +| sbt project | Scala | Spark | Artifact | +|------------------------------|-------|-------|---------------------------------------------| +| `standardization2_12Spark35` | 2.12 | 3.5.x | `spark-data-standardization-spark3.5_2.12` | +| `standardizationSpark35` | 2.13 | 3.5.x | `spark-data-standardization-spark3.5_2.13` | +| `standardizationSpark41` | 2.13 | 4.1.x | `spark-data-standardization-spark4.1_2.13` | + +```sbt +sbt test # all combinations +sbt standardizationSpark41/test # single combination +``` +Spark 4 requires Java 17, run sbt on Java 17 to test the Spark 4.1 project. + ## How to generate Code coverage report ```sbt -sbt ++ jacoco +sbt "project " jacoco ``` Code coverage will be generated on path: ``` -{project-root}/target/scala-{scala_version}/jacoco/report/html +{project-root}/target/jvm-{scala_version}-spark{spark_version}/jacoco-report ``` +e.g. `target/jvm-2.13-spark4.1/jacoco-report` for `standardizationSpark41`. diff --git a/build.sbt b/build.sbt index 41be870..d9b1ae4 100644 --- a/build.sbt +++ b/build.sbt @@ -15,15 +15,16 @@ */ import sys.process._ -import Dependencies._ +import SparkVersionAxis._ ThisBuild / name := "spark-data-standardization" ThisBuild / organization := "za.co.absa" lazy val scala212 = "2.12.20" lazy val scala213 = "2.13.17" +lazy val spark35 = "3.5.7" +lazy val spark41 = "4.1.2" -ThisBuild / crossScalaVersions := Seq(scala212, scala213) ThisBuild / scalaVersion := scala213 ThisBuild / versionScheme := Some("early-semver") @@ -32,23 +33,25 @@ ThisBuild / versionScheme := Some("early-semver") ThisBuild / javacOptions ++= Seq("-source", "11", "-target", "11") ThisBuild / scalacOptions ++= Seq("-release", "11") -libraryDependencies ++= dependencyList(scalaVersion.value) - lazy val printSparkScalaVersion = taskKey[Unit]("Print Spark and Scala versions for standardization") -ThisBuild / printSparkScalaVersion := { - val log = streams.value.log - val scalaVers = scalaVersion.value - log.info(s"Building with Spark ${getSparkVersion(scalaVers)}, Scala ${scalaVers}") -} -Test / parallelExecution := false -Test / logBuffered := false -Test / fork := true +// Major version of the JVM the tests are forked into (Test / javaHome if its set, else the JVM running sbt) +def javaMajorVersion(javaHome: Option[File]): Int = { + val version = javaHome + .flatMap(home => IO.readLines(home / "release").find(_.startsWith("JAVA_VERSION="))) + .map(_.stripPrefix("JAVA_VERSION=").replace("\"", "")) + .getOrElse(sys.props("java.specification.version")) + version.stripPrefix("1.").takeWhile(_.isDigit).toInt +} // Providing Spark required access to encapsulated JDK internals when running on Java 17+. // to avoid failures IllegalAccessError (e.g. sun.nio.ch.DirectBuffer) & IllegalAccessException // (e.g. sun.util.calendar.ZoneInfo) , They don't affect Java 11. -Test / javaOptions ++= Seq( +lazy val testJavaOptions = Seq( + "-XX:+IgnoreUnrecognizedVMOptions", + // keep messages of implicit JVM exceptions (e.g. ClassCastException) that tests assert on, even once JIT-compiled + "-XX:-OmitStackTraceInFastThrow", + "-Xmx2048m", "--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED", "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED", @@ -60,10 +63,22 @@ Test / javaOptions ++= Seq( "--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED", "--add-opens=java.base/jdk.internal.ref=ALL-UNNAMED", "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED", + "--add-exports=java.base/sun.nio.ch=ALL-UNNAMED", "--add-opens=java.base/sun.nio.cs=ALL-UNNAMED", "--add-opens=java.base/sun.security.action=ALL-UNNAMED", "--add-opens=java.base/sun.util.calendar=ALL-UNNAMED", - "--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED" + "--add-opens=java.base/sun.net.www.protocol.jar=ALL-UNNAMED", + "--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED", + "-Djdk.reflect.useDirectMethodHandle=false", + "-Dio.netty.tryReflectionSetAccessible=true", + "-Dio.netty.allocator.type=pooled", + "-Dio.netty.handler.ssl.defaultEndpointVerificationAlgorithm=NONE" +) + +// preventing Java 11 JVM from starting (unknown module / option) +lazy val java17TestJavaOptions = Seq( + "--add-modules=jdk.incubator.vector", + "--enable-native-access=ALL-UNNAMED" ) // Only apply scalafmt to files that differ from master (i.e. files changed in the feature branch or so; n/a on Windows) @@ -80,16 +95,47 @@ lazy val fmtFilterExpression: String = System.getProperty("os.name").toLowerCase ).!!.trim s"diff-ref=${baseBranchName}" } -scalafmtFilter.withRank(KeyRanks.Invisible) := fmtFilterExpression + +lazy val commonSettings = Seq( + name := "spark-data-standardization", + printSparkScalaVersion := { + streams.value.log.info(s"Building with Spark ${sparkVersion.value}, Scala ${scalaVersion.value}") + }, + Test / parallelExecution := false, + Test / logBuffered := false, + Test / fork := true, + Test / javaOptions ++= testJavaOptions ++ + (if (javaMajorVersion((Test / javaHome).value) >= 17) java17TestJavaOptions else Seq.empty), + scalafmtFilter.withRank(KeyRanks.Invisible) := fmtFilterExpression, + // JaCoCo Method Filter Plugin + jacocoReportName := s"spark-data-standardization Jacoco Report - spark:${sparkVersion.value} scala:${scalaVersion.value}", + jacocoReportFormats := Set("html", "xml") + // jacocoExcludes := Seq("za/co/absa/standardization/udf/UDFBuilder*", "za/co/absa/standardization/udf/UDFNames") +) + +// Aggregates all the build rows, has no sources of its own and not published +lazy val root = (project in file(".")) + .aggregate(standardization.projectRefs: _*) + .settings( + name := "spark-data-standardization-parent", + publish / skip := true, + crossScalaVersions := Nil, + Compile / unmanagedSourceDirectories := Nil, + Compile / unmanagedResourceDirectories := Nil, + Test / unmanagedSourceDirectories := Nil, + Test / unmanagedResourceDirectories := Nil + ) + +// One row per supported Spark version, each published e.g: spark-data-standardization-spark_ +lazy val standardization = (projectMatrix in file(".")) + .settings(commonSettings: _*) + .sparkRow(SparkVersionAxis(spark35), scalaVersions = Seq(scala212, scala213)) + .sparkRow(SparkVersionAxis(spark41), scalaVersions = Seq(scala213)) + .enablePlugins(JacocoFilterPlugin) // linting Global / excludeLintKeys += ThisBuild / name // will be used in publish, todo #3 - confirm if lint ignore is still needed -// JaCoCo Method Filter Plugin -enablePlugins(JacocoFilterPlugin) -jacocoReportName := s"spark-data-standardization Jacoco Report - scala:${scalaVersion.value}" -jacocoReportFormats := Set("html", "xml") -// jacocoExcludes := Seq("za/co/absa/standardization/udf/UDFBuilder*", "za/co/absa/standardization/udf/UDFNames") // Command aliases for JaCoCo coverage workflow addCommandAlias("jacoco", "; jacocoOn; clean; test; jacocoReportAll; jacocoOff") addCommandAlias("jacocoOn", "; set every jacocoPluginEnabled := true") diff --git a/project/Dependencies.scala b/project/Dependencies.scala index cd3d63d..69ad413 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -17,22 +17,16 @@ import sbt._ object Dependencies { - private def getSparkVersionUpToMinor(sparkVersion: String): String = { + def getSparkVersionUpToMinor(sparkVersion: String): String = { val pattern = "([0-9]+)\\.([0-9]+)\\.([0-9]+)".r val pattern(major, minor, patch) = sparkVersion s"$major.$minor" } - private def sparkFastTestsVersion(scalaVersion: String): String = "1.1.0" + private val sparkFastTestsVersion: String = "1.1.0" - def getSparkVersion(scalaVersion: String): String = if (scalaVersion.startsWith("2.12")) - "3.5.7" - else - "4.1.2" - - - def dependencyList(scalaVersion: String): Seq[ModuleID] = { - val sparkVersion = getSparkVersion(scalaVersion) + /** Dependencies of one build row, the Spark version is provided by the row's [[SparkVersionAxis]]. */ + def dependencyList(sparkVersion: String): Seq[ModuleID] = { val sparkVersionUpToMinor = getSparkVersionUpToMinor(sparkVersion) List( "org.apache.spark" %% "spark-core" % sparkVersion % Provided, @@ -40,7 +34,7 @@ object Dependencies { "za.co.absa" %% s"spark-commons-spark$sparkVersionUpToMinor" % "1.0.0" % Provided, "za.co.absa" %% "spark-commons-test" % "1.0.0" % Test, "com.typesafe" % "config" % "1.4.2", - "com.github.mrpowers" %% "spark-fast-tests" % sparkFastTestsVersion(scalaVersion) % Test, + "com.github.mrpowers" %% "spark-fast-tests" % sparkFastTestsVersion % Test, "org.scalatest" %% "scalatest" % "3.2.2" % Test ) } diff --git a/project/SparkVersionAxis.scala b/project/SparkVersionAxis.scala new file mode 100644 index 0000000..9269a45 --- /dev/null +++ b/project/SparkVersionAxis.scala @@ -0,0 +1,53 @@ +/* + * Copyright 2022 ABSA Group Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import sbt._ +import sbt.Keys._ +import sbt.internal.ProjectMatrix + +/** + * Spark version as an sbt-project matrix axis (inspired by spark-commons). + * + * Each row of the matrix gets a unique project id e.g. standardization2_12Spark35, its own target directory + * and an artifact name carrying the minor version, e.g. spark-data-standardization-spark3.5_2.12 and + * spark-data-standardization-spark4.1_2.13, the builds for different Spark versions don't collide. + */ +case class SparkVersionAxis(sparkVersion: String) extends VirtualAxis.WeakAxis { + val sparkVersionMinor: String = Dependencies.getSparkVersionUpToMinor(sparkVersion) + + override val directorySuffix: String = s"spark$sparkVersionMinor" + override val idSuffix: String = s"Spark${sparkVersionMinor.replace(".", "")}" + // Scala version suffix e.g: standardization2_12Spark35 and not standardizationSpark352_12 + override val suffixOrder: Int = 200 +} + +object SparkVersionAxis { + val sparkVersion = settingKey[String]("Spark version of the build row") + + implicit class ProjectExtension(val matrix: ProjectMatrix) extends AnyVal { + /** Adds JVM row for the given Scala versions, built and published against the Spark version. */ + def sparkRow(axis: SparkVersionAxis, scalaVersions: Seq[String], settings: Def.Setting[_]*): ProjectMatrix = + matrix.jvmPlatform( + axisValues = Seq(axis), + scalaVersions = scalaVersions, + settings = Seq( + sparkVersion := axis.sparkVersion, + moduleName := s"${name.value}-spark${axis.sparkVersionMinor}", + libraryDependencies ++= Dependencies.dependencyList(axis.sparkVersion) + ) ++ settings + ) + } +} diff --git a/project/plugins.sbt b/project/plugins.sbt index ab827fb..789754b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -21,6 +21,9 @@ addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.7.0") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.5") addSbtPlugin("io.github.moranaapps" % "jacoco-method-filter-sbt" % "2.2.0") + +addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.11.0") + lazy val ow2Version = "9.5" def ow2Url(artifactName: String): String = s"https://repo1.maven.org/maven2/org/ow2/asm/$artifactName/$ow2Version/$artifactName-$ow2Version.jar" diff --git a/src/main/scala/za/co/absa/standardization/Standardization.scala b/src/main/scala/za/co/absa/standardization/Standardization.scala index d59524c..0f180e3 100644 --- a/src/main/scala/za/co/absa/standardization/Standardization.scala +++ b/src/main/scala/za/co/absa/standardization/Standardization.scala @@ -70,17 +70,46 @@ object Standardization { * by default , this throws errors and aborts the job. This helper temporarily disables ANSI mode during * plan construction and analysis to preserve the same behavior (null-on-failure) for both Spark 3.5 and * Spark 4. The original value is restored after. + * See ADR.002-ansi-handling for more details: + * + * - DataFrame analysis is eager and expressions `Cast`, `to_date`, `to_timestamp`, and arithmetic operations + * capture their evaluation mode during analysis, so the DataFrame retains non-ANSI behavior even if + * execution occurs later with ANSI mode enabled. + * + * - Concurrent calls on the same session are reference counted: the first one saves the original value and + * disables ANSI, the last one restores it and overlapping calls never see ANSI re-enabled midway ,the + * original value is always restored. */ private def withNonAnsiCasts[T](spark: SparkSession)(body: => T): T = { - val ansiConfKey = "spark.sql.ansi.enabled" - val previous = scala.util.Try(spark.conf.get(ansiConfKey)).toOption - spark.conf.set(ansiConfKey, "false") - try { - body - } finally { - previous match { - case Some(value) => spark.conf.set(ansiConfKey, value) - case None => spark.conf.unset(ansiConfKey) + NonAnsiScope.enter(spark) + try body + finally NonAnsiScope.exit(spark) + } + + private object NonAnsiScope { + private val AnsiKey = "spark.sql.ansi.enabled" + + private final class State(val previous: Option[String]) { + var activeCalls: Int = 1 + } + + private val states = new java.util.IdentityHashMap[SparkSession, State]() + + def enter(spark: SparkSession): Unit = states.synchronized { + Option(states.get(spark)) match { + case Some(state) => state.activeCalls += 1 + case None => + states.put(spark, new State(spark.conf.getOption(AnsiKey))) + spark.conf.set(AnsiKey, "false") + } + } + + def exit(spark: SparkSession): Unit = states.synchronized { + val state = states.get(spark) + state.activeCalls -= 1 + if (state.activeCalls == 0) { + states.remove(spark) + state.previous.fold(spark.conf.unset(AnsiKey))(spark.conf.set(AnsiKey, _)) } } } diff --git a/src/main/scala/za/co/absa/standardization/adr/002-ansi-handling/ansi-handling.drawio b/src/main/scala/za/co/absa/standardization/adr/002-ansi-handling/ansi-handling.drawio new file mode 100644 index 0000000..b94a05c --- /dev/null +++ b/src/main/scala/za/co/absa/standardization/adr/002-ansi-handling/ansi-handling.drawio @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/scala/za/co/absa/standardization/adr/002-ansi-handling/ansi-handling.md b/src/main/scala/za/co/absa/standardization/adr/002-ansi-handling/ansi-handling.md new file mode 100644 index 0000000..cc6a778 --- /dev/null +++ b/src/main/scala/za/co/absa/standardization/adr/002-ansi-handling/ansi-handling.md @@ -0,0 +1,130 @@ +# Context + +Standardization library converts raw input into a target schema. Its central rule is **a bad value must not +fail the job**. It becomes `null` in the output column plus an entry in `errCol`: + +``` +stdCastError, E00000, "Cast from 'string' to 'integer'", num, [abc] +``` + +The library implements this by casting and then checking for `null` in the result (see `TypeParser`). That only works +if Spark's casts and parse functions **return `null` on failure**, which is Spark's *legacy* (non-ANSI) behaviour. + +the library emits these ANSI-sensitive constructs on (`src/main/scala/.../stages/TypeParser.scala`, +`InfinitySupportIso.scala`): + +| Construct | Example in library | Spark expression | Where the ANSI flag lives | +|---|---|---|---| +| `Column.cast(dt)` | every type conversion in `TypeParser` | `Cast` | `Cast.evalMode` | +| `to_date(col, pattern)` | `TypeParser.scala:628,630,636,662` | `ParseToDate` → `Cast(GetTimestamp(...))` | `ParseToDate.ansiEnabled` | +| `to_timestamp(col, pattern)` | `TypeParser.scala:684,723` | `ParseToTimestamp` / `GetTimestamp` | `failOnError` | +| `unix_timestamp(col, pattern)` | `TypeParser.scala:699` | `UnixTimestamp` | `failOnError` | +| `col % 1.0` | `TypeParser.scala:399` | `Remainder` | `evalContext` | + + +## What changed in Spark 4: ANSI is on by default + +`spark.sql.ansi.enabled` default value: + +| Version | Source | Default | +|---|---|---| +| 3.5.7 | `spark-catalyst` `org/apache/spark/sql/internal/SQLConf.scala:3198-3207` | `sys.env.get("SPARK_ANSI_SQL_MODE").contains("true")` → **false** unless env var set | +| 4.1.2 | `spark-catalyst` `org/apache/spark/sql/internal/SQLConf.scala:4529-4538` | `!sys.env.get("SPARK_ANSI_SQL_MODE").contains("false")` → **true** unless env var set | + +The extract from config doc https://spark.apache.org/docs/4.1.2/sql-ref-ansi-compliance.html: +> *"When true ... Spark will throw an exception at runtime instead of returning null results when the inputs to a SQL +> operator/function are invalid."* + + +### Observed effect (with ANSI on) + +`standardize(...).collect()` gives the +following : + +| Input | Spark 4.1.2 | Spark 3.5.7 (ANSI explicitly on) | +|---|---|---| +| `"abc"` → int | `SparkNumberFormatException: [CAST_INVALID_INPUT] The value 'abc' of the type "STRING" cannot be cast to "INT" because it is malformed. ... SQLSTATE: 22018` | same error class | +| `"2147483648"` → int (overflow) | `SparkNumberFormatException: [CAST_INVALID_INPUT] The value '2147483648' ...` | same | +| `"not-a-date"` → date (`yyyy-MM-dd`) | `SparkDateTimeException: [CANNOT_PARSE_TIMESTAMP] Text 'not-a-date' could not be parsed at index 0. Use \`try_to_date\` ... SQLSTATE: 22007` | same, but the message ends with *"If necessary set "spark.sql.ansi.enabled" to "false" to bypass this error."* | + + +**Test data :** +``` +=== standardizationSpark41 +spark=4.1.2 case=invalid -> org.apache.spark.SparkNumberFormatException: [CAST_INVALID_INPUT] The value 'abc' of the + type "STRING" cannot be cast to "INT" because it is malformed. Correct the value as per the + syntax, or change its target type. Use `try_cast` to tolerate malformed input and return + NULL instead. SQLSTATE: 22018 +spark=4.1.2 case=overflow -> org.apache.spark.SparkNumberFormatException: [CAST_INVALID_INPUT] The value '2147483648' of + the type "STRING" cannot be cast to "INT" because it is malformed. ... +spark=4.1.2 case=baddate -> org.apache.spark.SparkDateTimeException: [CANNOT_PARSE_TIMESTAMP] Text 'not-a-date' could + not be parsed at index 0. Use `try_to_date` to tolerate invalid input string and return + NULL instead. SQLSTATE: 22007 +- invalid values, overflows and unparseable dates become errors ... *** FAILED *** + org.apache.spark.SparkNumberFormatException: [CAST_INVALID_INPUT] The value 'abc' ... +- overlapping standardizations on a shared session ... *** FAILED *** + java.util.concurrent.ExecutionException: org.apache.spark.SparkNumberFormatException: [CAST_INVALID_INPUT] ... + +=== standardization2_12Spark35 +spark=3.5.7 case=invalid -> org.apache.spark.SparkNumberFormatException: [CAST_INVALID_INPUT] The value 'abc' ... + ... If necessary set "spark.sql.ansi.enabled" to "false" to bypass this error. +spark=3.5.7 case=overflow -> org.apache.spark.SparkNumberFormatException: [CAST_INVALID_INPUT] The value '2147483648' ... +spark=3.5.7 case=baddate -> org.apache.spark.SparkDateTimeException: [CANNOT_PARSE_TIMESTAMP] Text 'not-a-date' could + not be parsed at index 0. If necessary set "spark.sql.ansi.enabled" to "false" to bypass + this error. +- invalid values, overflows and unparseable dates become errors ... *** FAILED *** +- overlapping standardizations on a shared session ... *** FAILED *** +``` +**See file:** `src/test/scala/za/co/absa/standardization/StandardizationAnsiModeSuite.scala` + +Without any handling, **upgrading to Spark 4 with default settings would see the first malformed record +kill the job** instead of being reported in `errCol` . Spark 3.5's error output recommends turning ANSI off. Spark 4 +dropped the hint, but the mechanism is the same. + +--- +# Solution design: `withNonAnsiCasts` / `NonAnsiScope` + +**See file:** `src/main/scala/za/co/absa/standardization/Standardization.scala` +(`standardize` at line 34, wrapper call at line 42, `withNonAnsiCasts` at line 83, `NonAnsiScope` at lines 89-115) + +## 1. How its applied + +```scala +def standardize(df, schema, config)(implicit sparkSession): DataFrame = { + udfLib.register(sparkSession) + ... + withNonAnsiCasts(sparkSession) { + validateSchemaAgainstSelfInconsistencies(schema) // step 1 + val std = standardizeDataset(df, schema, config) // step 2: builds every cast / to_date / % ... (df.select) + val cleanedStd = cleanTheFinalErrorColumn(std) // step 3 + ... RecordIdGeneration ... // optional id column + idedStd // ← the returned DataFrame: plan already analysed + } +} +``` + +This covers **all** plan construction done by the library, from the first `Column` built to the last +`Dataset` created. Each `select` / `withColumn` creates a new Dataset, and in Spark Classic that eagerly analyses +it. + +## 2. Implementation +| Design | Reason | +|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Change the **session** conf (`spark.conf.set`) | Found as the only place Spark 4.1 Classic reads the cast mode from (`SparkSession.scala:882`, `columnNodeSupport.scala:47`). | +| **Reference count** per session | Overlapping calls share one ANSI-off window, doesn't restore while another call is still planning, and the original value is captured exactly once. | +| `IdentityHashMap[SparkSession, State]` | Sessions are keyed by **identity**, so two distinct sessions sharing a `SparkContext` get independent windows. The entry is removed as soon as the count hits 0, so sessions are never retained (avoiding potetial memory leak). | +| One `synchronized` monitor, held only for `enter`/`exit` | Planning (`body`) runs **outside** the lock, so concurrent standardizations still plan in parallel. | +| `getOption` + `unset` when absent | If the caller never set the key, we restore "not set", so Spark keeps using its built-in default (and any future default change). We don't freeze an explicit value. | +| `try/finally` | Restores even if schema validation throws `ValidationException` or plan analysis fails. | +| Private, no public API change | Callers don't need to change anything. | + + + +# Outcome + +The returned DataFrame produces null + errCol for invalid / overflowing / unparseable values, even when the action runs after standardize returned with ANSI on , the concurrent calls on a shared session produce correct results. +The session's ANSI setting is restored to the caller's original value after the last overlapping call. + + +![Architecture diagram!](ansi-handling.png) + diff --git a/src/main/scala/za/co/absa/standardization/adr/002-ansi-handling/ansi-handling.png b/src/main/scala/za/co/absa/standardization/adr/002-ansi-handling/ansi-handling.png new file mode 100644 index 0000000000000000000000000000000000000000..b34b53f280af44a99af74c2c7461754d84033072 GIT binary patch literal 137590 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 eZ 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�&slp&6G}a#6p=UZStW^za{h8l)Wh5{Q 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%0AVQ_hFO7Rg~F4#32MjzMwY%Gab(PM~w; zUr_FfdRt-o%MWnB2NeZ8D?R;~HU|h>RR#f(TenZiBy8q=4_Ay&XD-QhgFU(-HnKMHdSyg)=@~ z#U?pb>;kG`nMKeAiJkXs#3NL)@%}sFk->rY!aMjjNuNcSzm6_#(SB7J|9^)zEc2IU zDfPOcS<+?+d39(yyR=tE;VJju7kEx}gM74x-Oub|H7r?a_eHYfL_bW~?Y?|%2UA@o zm^tw^v42M}b9lK7(id1|4HtcY2G(qu&oPYw=^Ci>)51`pwJb&KSCluRtH|T)h$aqg zl|xXTR$s#+`b*B<>*v`1&U`(n8WuV~ha zsxse>5Y2CKYWu2QEl-O0mDyp3 z6#x<*)>s6gWL2%!#)aOUPj|!9b4}4m-%!>UtZ`v6t!MTcy-Y!_G5deVRR!}=8~+4~ zuU=8vYzLwN_n1xUi0{E_o%gZRT5Vw0{|yz$V%SbYfRlH9{|~ccAhG$oCUWR={IDqN zlS~(Xomq`1u4or?NsB@bN}(fLOEtu7UFSH?q>ywCR3G8C=N6)xkE42VyN58?1G3j$c9+Cbv!N z4yhU)-=QRWdQiW3$WOI`76$%?@e{MoqY zH&%=hn^9SvcK!z9od;V#pW6#*&gV%N)@dvDJ?QXn_eUpZP-D%nXW{}e0x4sywz=ol zmLhWz-W9;69RMbFM`}3zBmC2a&o^J4J+!8MYx@w3lRuh0?)@6cm!0t=Ub~XHxvsbA z=MXbFk#wl#zo}!*_rmTFHia#!&#Kja&>wr;L~)+l+P*2zsIqLJVVEyr1iuMqTXw+s zyHXQyD{@Ix&WHEopDgVWkvNtcy(53emU^4J?j|NCF1-3eA=S;B!jMAgvBaU#56|mQ zg|EJ@Ks++4E!BYGS==ig;_0IwHR}x>+}ToI{Z*S!?1Y?DXaYD`X*%lsPc!kN%8AiU z>K=HDohMmik^k_WkgUIg^gVOl@ykSebV#_i&31-7#dpV4RIk^=`I50qV_lZ>f0Q<7 z5{r5Q8PV86>)!cj`+_ltbTL|Hb(%|_lf9h*t3Lkzo_DOilh_WBjP+LW0>=J2tEjBx z@cm?~!oA-#H=Q|ow8r(+r5exev7_1P#X?78cjR}v)on*bXgrzE;fgG)M8;AYo7J_@ z@aV{wm#A}W%-ef4f4a-(_+-ZLteFOW!ZvFD>~SYO0(#>S?8DV{3?F#XVHqijJ8jp= z)_feSdplgTm@k>PLEzW4B;L`M$HKMweir_QxBn6CD$Bs^XbZ84dfGH^v{kd5GG#m- zvE(3=`O(jAe&Uaa*}MenuHlk*Qj>9^B?F<)aW224tGx#NRiPZg#*$nDrx%jA$U~PWcR#9%~ zoOihEkFevv&-Xe+%$7#Z+Z@N0=NGRn8_`Ruw#blKlEBO%(^2%EJc0k7JvnzKQ0&Wv zFOPO6-u>A~NI=c5Efce+YwpaOA=6`}&VQ=nf2`kfY`RRtuFpsMY)$yl$(~TEc?0c~ z;N-Mt0fO|n&4BcFZ5<&xkMsVB-ii6yUgG^9KRLC(=X^kaD9LeN{eY`Ghvlt@`El5< zGi_k}_xyaZF9_$-8a(gNTOcw)E%VQIN|?v@PKy{d+MZTd-Ww<__w?HSs_-Oj*o{A_ zXNK*}NV4aQg+$UE*p?dh>a<4&e|dZivztq7yqELCc<-(c0s+_NJu3cNq>LX?#nzz~ zw!MW3(e}N^E06n~a~y)#*+nn%icR5yC8Uzlr=+_U|P6NfgrLv%}!Q4vYY|`WAkZZG`8$uImFUS!QrE({k{4a|StG zd^qXUy;m;ZgW}jNIW;+jo+&wvTF|UE3l=FRS~YLwAM1H^K;Y`XY!AaiB|eDzoV@8_ z;n*|U;i?{mL&d!J)Ks%xWNwB|$Nid|Q#ifXZ)N`Mwy$m0YF7F;t}(@Pt;Z|rD(4K- zn%N%Ds^+kwc={G?Km13aE&X3NWfa;5t_L)$hnt*tirk46NebRT{C#sIbEjp>r*gBS z{f3UE0=htuu}_J&CG>Am$I78*Gv3bK9v$&)(b#^ZYl*eOSLRGi+5!(PxYNp$ULAhw zrUfFG(8JEV{c>cmj{Xaq0`P?TS8%l(61BGT#UQOTEX9kUpRdF=js(5eFSXAxGc?a~ zOPDWnvOkSeM#g`e`_h1)kB`E0q{^oC9AARQUNO^a*`_h4_iZNrw)HbR{@8(O+EcyR!_%iRi!uP4$k>&QU);hthM`PO!6^PvTeN8gnI#dBC1*wS?G?DLSph26>FC0bbi_oJZZdSezZ(f`o9tby-^>z+R&CEC3B6=U)WyFTpFPy zN-{Jb{Uo{|{}R>cz?CPKh1)-bA!kb?=C2&@h?uuOJ}alOW;j-qKMnDDM~_JiES~&wVS}#5 z_TTI;23z+;SeM7_&$TkD(cYj}vqX~ms5v*UaVyquWZo{0B!#AC78Laow8$Qv;z_;> z-jsr|pyQMGjHi^d{=5 zldlnlPp8r~Gj(ESErdNv8QwMCj9L9Qhb~cCW?Pz0!t0gtQrLLNTSBF1j7HU@D0>bH@Jr&HRSuAc7^ zv!*L=3p_PvG_hkkdM++cY3x*zGc|gyAcYp?mu^MNdeN~Ob;-KPp~Pe^@iMr#26)cc zay1E$i%~oB$xibKA;N|x1+KAQbqeB!^~Qe8&Y+bri`}swQ9LS2wX9eGvqq2j1{0cp zEXfY;UG{(3FE`ev>ej@-@agr5v*emhc10qNn zUArX@vyU9WLVzYgjL3EGtCf0fK8bT2Zfo&BrkVLS(|?+qFAKv0F<+g&j6U`@>g|Kl z%GHkd;7TASQ|1Ec65vDteEeXM*{*@LYD9^RYna62U~{~VEQ5=acsT1;J3=C_cn%&! z26c-ZL+Jm3o%Z99js;s9`@_@q75D%KUxzRkO1)5TJ&J#>WM*B%UwCf&%dd%}9gdfj zGlqyM4M36EV_qQJFUpz zEJptit;ht+NkxqpHxyC~-hNwUFY*Bz4OB8ulWq{caKm0&mG9cH$o>nN&xOtg|J{T~ zz8@Bt$yBXBCB@dWm^rNfJ}yJRhaW7mI5NO?GFcrKQqjTXY9Ri;Qct#SSU4Q5iunH- z7F{WR{l9%%_<}YkErWEeXa_uYp2HH9j8p{cu#LWDAL~5g4|w|ls~y;}fuNZ?0_+h< zKwksNRoa={EW*IE6MqL>Q6#UNvXMU{+P54*I|$vzaK9d9gXjju2t-vb%e&PKIJU-Z z;*rff?C9pJ+=hM*mg@>pnB2yG1&-CfL)gteHY>1m{wE08?oyXtQz5o3k?64(RGYS2 zifIU-p@JmK;4|OG(y+5XN!YAC&(t?W1?2$NeDGaHqjGL3L;6SdJS<3Z6(lh$1Tv**Q3Fs_ISZ~+%&*| zy5tuW>?QoHbojg~Th;CI>;g%DqTb3&Un;$U!t52dC{uLn#qc8dl2Nok-34g9SQwvY3bFEM?U_K}cdpNbxD@ zd2~8mrWKkfLez7eta`wdkNN!(F!>gxqxkh#w~?l#s0nfNlJ-F#uFPUeQ4~!_54SKi zMEQb1U78?$zI*sP5M-+2^LgAC{UC2%Yx~X|24wU&Bc1-78AuX&$xM?*mERrOF^dKD zX-GSeK@vKqk@RdQzn;WJmLXhKSGgy^g*(@GbZHWiB#GjuFG9s9%{3#xK}w2@Y>&I% zz2Nht>C%V8jR}+a+}=nZIW5Yoy0}NYzF$roSTNLzj~Qf~Fls%;<|Tn6{0s@J5ud=) zyyy8Qz$*zw=Y3LbcCT>jSGXwb+R8h<*&WV3(BgHTGYs;D45lHd;c1!arQQob!e%jw zfjtvnmVwlHg8g5t@r@<4U6Fg<@*{n{vGXtClpDfdon;LSLZ3<8_xYfk)xY@^h%o>s@cnh+SP>TDMA!o|d_f=!P`)MZA*;k{=GN z^uwF!;OA1z(x2CQmr)M|rRo$b8k=bZYVmeN1W&i&jcgQV^Q;tRMT{~9Y10lJvj#ai zW{oHDf#Z7L8_kJ*&K zs)s66%T;w>{;+J9?GMH5`=7oG;8F-k03i9`yUfR5**2Q%dr%E4b)~TvL2=H=-CK#5 zKHqtrRgaA5!(AbWHFoYc8Tjf4>W!c~cR~)=0kdlF(H#d|I}@D(71Acs=wNbvbMo>& zBqoUFC_V4t4^nJOtm{mnTti7xGA{c(aQjtJFFRxXw!(qp=Qe!jj()gxeG3DGNI?5X zj`hnk?~i`KKuVX~Af3@maLo2(Nq3kR05W@cVdFo@$E|>?w;$yQS#|qa{4Cb*2Mg`8 zG~T$Bww0IrjE$0qT~w93hKQDk!ccdiTRmuNf|<|IunGegLe=%`)Km$MJO7+%2JN!r zZcn}(2o$pRR_baVg1RoJ`Inv|3Un(|x(qb2UMQnuWTxA1I7yeP3`{igkEI?P>Rig1 zgl>~|OVmt;cjJ?}gd3r*6O!^S2i3u_)6o02A&MCrR1h04K2#Anji$Ohj_4@^{`pA znwEO!ne;*<)q128W=sDuv!wx*CClka#?qu&4CK$2;-HXZ?o7?hSI9h(fn+xG0+m`v z;n?DvU$0IB3UK0m=coy1%SsiMrLRH7(>4cr35z#!N=r-kS_xe%0Zi){WNj;)?^Fap z*Lr9Q-Hb1{s-!|&)SR)rhN0Ayc>6N?go_tVIvghX90yHgiv}DRn8l*oodjk2Of%V5 zg=S>?P}#Y7B3~>E|D|G6v87ZdQl3ZKQJ;^K&7Z_i`e;#WZf5%ePb$aYRzc~Ey9&fLL z&=I?as14#DEngL*Z$XMmySa;zj@g-F3n)0NCoEPGPC&}xnGpHJ-^tm~fBgI7$Q_#(Fyp{W=P47NP=(7&*$go`Mzx|_bslg}Lp_;G; z+e}ycBtqFt0W2ZSd1;S7wOBq9arYLvWjH0Iqk-#ZgX^jTjXmB;j64Lx3!G9c~9&Nae3pGMQkR_;095T&rmO&ELTC8WBgR9g-tjR#1NrPdZUC5JZs zai@b4ap#YHWA2PXekr%w7j}f#+hyw%m1)Tul!tXCyf^!arRVcuspLC=F*RybR4<(S zIzApFh8tJ-IP6}^%s7y{NMbh^+K62}68kz(>|s@khDZZPV)i1`EUHTFJ59Z{vu#0J)Ebhoc(!U+ug2s!Y@Y&}E@tud|0j zZEkgsH(};Hp=D6=vt!EA3G1RD^qr*f?hld0d)=z8pxzn{zcssyp}N-1EOq}L`QmU3 zV@C3x)37h@pifaFu1Zj#{@c|<)9sB8KC#p~**-H|tYz-1tpPs_jY>uw1Ws@e_Ch<1 z+nm%R#*{znynfN*u@hbRU#NA4G|wV zO~OJ$?ZC{FVO~VU(@d0;k{=vz>f3InfcUubJAE;i6eL5MeDm9x! zRY()tm{~e!9P*_gv}y4p@6wg|&%R~FQ(2Z1!V<0=&8vGXVquBgNQiLraMshU0xjy- z!mDoUah*e&!aI%fcX%-R7VG=mJ?!-6XB(%$LZ0X_sJ(GEUn4^7qzd(YP;@N8oYzCt zgAk6{GbYDHoefGjYhDGVQijX1s5ib(VaX~58{6IF z^0{-{1=2pwg<$P|Gi$bWn(abLbdQ(3zu8XSiH8z@KI zJJ(M;4zgWiL-2wLBp85rSoz27l~?u?;%}Qjui-I}Zb6g+yp;L~6wYzJk+IkqqgZ63 z4i18pSpNnW?%30gW)I9; zO3_#bIn2O(q;u#!WnyBYbf-iLeMp?pfh3NO&DkP#-Vvh?HGNAHeKJT-lr{=ynKHRq>gXxp?{FRPWPKc_a%uS2o_LYU#Dg;@Rm=hfI%Q#9i6j={M=q*EPm3Ga_x- z@O9t$O2qp-6e4LuN2-A4@ek}|T^i(-ty^dR)ToSEGKWeJAwefS*M`@ouDzxy0kZi2 zB!;WMuESDmH6?uk-!mBWTn&tcjMotxAJi3Hb=b!-Q)Vo+*mq&dyr(1#IR^G+#L7AwQM%rDy@c}a7$lYVd3B^z zzF~{Jb=1V2hcBvmn!iDfKVGgS4{$Wi6(_qmNf2SrUMHIoO`@Ie0ka-M_s(;R@aM z3!k3`Mejdly&02~0Y}L+N|>J+D)XQl6rqn#G^)RPoe9-DW;Y`z4miJjqShZZd%MP6 zC2s=pNRiei^)pLIAJcl!e#XfTi>_8{)PGEGP6I^>aSv0qE_8+mB)dCIlki&xwzyegBCe!TLw+pdD~e+kJUu?1yf zQj?A;R@zY}t-B#iEreZxy`BHEvp1%qHc|A2{?vLIZRvXN6V#P=^hicmg?PmreEGH=DN;I#6Hvs@Xh6F9CQk`l zTWXx_u>8I8=gR4|WetujcCwb356aSFmuet=OXkSpXxt4<9`&r|1$}k-CbJ;=06Bel zM``znEI!^bMVXcoRBuKo{6UgqaHed)SvX76AM!sK-=?g> ziTaKCdzzNnT#^>SfIlT#Wh^x((RDtCf;4N_IqL#*>5c-H*U1b^q0mso3= zYaKWt(WhFCRUD!ezzw=V4xwW{aw)e?Cs7^;7Y-{Nm#&O0$j1*pIv?GhSg!bQJoTG( zRPc+T5{$N6#?sdj;jYxi*yb_|-3Z?8>M^4$t38n5MpmAH<=UAD>JpAR z7G=VSoQcrwf|n2;YEX4<8V|?!&ms*OCSJK-Kvk|~+%k93JpvGb-#I8%WK^KceR1f< zCtT9TdzdF&x`IcayG34(z(YvNrR>|M`^j@gv}ZJ~o5@=FwqbX!V#reYMFf4bxi&Ye zsIM(^NYufjJ1?l^L$Gp_r+)QIz0tC?wgGS^dSpxM%0fq|r;-mQ=vL`>X$48{NPSL| z?mcLrL48hx^MJ+NY>*m(nYis&Jd;4}v<;URs%i}`(@O}pL?HH0=8z4#OxEq(&75@! zq;}XPEX!Y!*;68ZXZ5W^ha;qMFS6&j{p|~jE7Pk^o7~@Q?aMT{AWsmp+{^De7f02)6-a6at+y=x=YC`( ziS`XD+GD_dBt!L~6BLt$IM^Unizh_d#tobQMzAKJP|GA*w9Wm`yX&vFnGtJSAQ73Q zyblox5JrewDATwV))%&d`t$h{L@fZCyNvH>HRZXJjgLZH(Ix*`+Nbd3S!#RW?j0Tfz_UK!;&NH zPKFi(>gvuFnAK?0bk+5Os&C*fDFy{ZWCKD$YHgXZ9~AsiW^Yis3mkFX~}qYexdF6Rmj!%mM{Y{$lP(o=DjU1`C zLP_(57y^L8*eD#aoMTKylsh7hvj+`==`qYhGQcskCYn)os$aTlqH58hNZ0LXsAbDl z5TZ%ng`lAnv>}6bDNcTKISD5r-fh%IWb|+%q|7U^uw% zv_ic-+PP>XnGC_nSlUs~GZg`%L~OW-M=3?a>^Np?AH*evIQkc+hl@x5ud8d1hcfTt zV|Iw9x0_0LrA*1C8j)V@i&n~n!oJI>VZ1I8rl>|Jx1z08FO7^_;oaEe8e{C3r72q) zgD%Pp(d%bB@6Sz*X1#$G?cEGu&HrVxD(Y z&42N{z~5kFdh18|y(7Mmn)9E1FDYK*S%`11$IcMxgoe07C|4AI{qT{APj%X%!Wp@S z#_N;C3ODgSWP0i%;~Gn_YMSae&Y@_*mI*l6rwb|UA{KOg8Na9L*0u1@0b#)#5_jFt z8dxNtBuWp^(pUi|rYnFdrtl!BNbpBoEnKKco?{c8t$n)AORrp8C5wOjI#_eppQC5X zgr&brwgph=KI|LYjOLlcmvCm8<6Ehc1`Rz62+Tyj&Tkn>JzG=0Gsipj$s1NGTVHcq z%k`{t@2`A((HDNes|{CZ=SLK}D#IL9Ggv+KaUPs5b`M@X*>k?16hG1a7-5zi-WZC< zr8eBUR)LqQlM9{Yj1RSIsxmKQrn%3Ew^;ZixrvXC{g`X5S=sm7RiIP^-*hRfHLD79 zlc>aNSYed?gYp=GW&3n&x#A&6Mod2L-agV$1GBVq~MO<79$RYR-^IjuvY~{%k&y%{L zJG&hF_Tor{-Wm&hj(1-~;P z6?B&=2;T@^IN{ipP}80P+vgjGK1DjNSB4^;Yz3a04now|=DC9`gW{`kchVPT#-&d#SfEla zr7SN8gt%aa81joTjY@6nW9%WLM`>HW8>!^Cw*1?MCfh=Kh`%{1?V{2pB_KsDfk=3- z*+ZpkF0Vs2#x&fJE&AK#nnGKDC7#>dau=&8RYpl495V3f4UmDyX+7GZECow)CfXM^ zx)^&%+fpX=IbRhw%-ATM!61`|KD?r0^>%-_SehmuJ~1m7xq{uEP{O$MH#krnl*mbk zS`fZ?-%3S8N<`3|%XY{b3zzny(NFE8R8x@xAmDn)z$o>L7=r}m6C3{;_KNd$lGF>L3Oexze@H??)A@<;?&htpUl%zL>=5oDZjrpFj=DLea8FY?~ zI^)Xw!qWTf+WXTD=ki2v7p*C;5k0!Yz(y(1yi+3;E7QmAU^|C`1ub`i5`pZ?=-178 z!(L?SkV+v*M&kO?gquyXxIj}PyQrV*EiEpBTP;^WPA!X!+vi zr4=;p9d5 zi2Fst+jh7E8mlUg7RpzmkkxXm8C1aqYy;oFG0v8})BiB(5V}73Wy{&n!%Qn~9M?PY zAdxWswA(%UhGlNbn4r2eckr2ZjN|;z9^DAVea-rh)!PYrCa>O7`q}ujfj>&xI=8=i ziM}bi0c^#vC`#pM1)~5dw*-90)T%P%KvW+9ll5`jm0q;^+-6jMZoH1k)>iB`&Hvfq z%ya6!hd->lsd0>;M?yIhTCqNDla%j0?H@3v2UO`q53L2AD@!chk2mK^(hEyoR~h)c z?Pp<^2`>m~TzZLw`2btN2=S?W1#C?k)5wQSYI#{axoHPprCHgUD@EPhcKmb(vIX=`g3*qg*kM?-OwwJN+^|#XTRmjr{=@yg{bD;ov=l zKrfYt4BDl~RI0TN;AkQ^9vBg@6^M-ile$(84?fr44nQ|xet<=JzVI$9cSk6OdOjZ6 z#QG+xkm7N7Pc`YgqS}mtU83?*CU{!@NO@2LHCu7D#dO^OC1|%pk3$O>Pc2 zbAA-QmB{pZqqW|bU18*$D+w>?pXQ6H!Wxe@dj)pjfqduRmF|%yKtwCi5Kgr`*68_$iwE(MD;&u@6iOm-^6-mCp-^gwzw;ex X)%ajro>BIG_ylFU-@&?gA365_Uwuxm literal 0 HcmV?d00001 diff --git a/src/test/scala/za/co/absa/standardization/StandardizationAnsiModeSuite.scala b/src/test/scala/za/co/absa/standardization/StandardizationAnsiModeSuite.scala new file mode 100644 index 0000000..bdf8da4 --- /dev/null +++ b/src/test/scala/za/co/absa/standardization/StandardizationAnsiModeSuite.scala @@ -0,0 +1,122 @@ +/* + * Copyright 2021 ABSA Group Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package za.co.absa.standardization + +import org.apache.spark.sql.types._ +import org.scalatest.funsuite.AnyFunSuite +import za.co.absa.spark.commons.test.SparkTestBase +import za.co.absa.standardization.RecordIdGeneration.IdType.NoId +import za.co.absa.standardization.StandardizationAnsiModeSuite.AnsiRow +import za.co.absa.standardization.config.{BasicMetadataColumnsConfig, BasicStandardizationConfig, ErrorCodesConfig} +import za.co.absa.standardization.schema.MetadataKeys + +import java.sql.Date +import java.util.concurrent.{Callable, CyclicBarrier, Executors, TimeUnit} +import scala.collection.JavaConverters._ + +/** + * Standardization relies on failed casts producing `null` (which is then turned into an error-column entry). + * Spark 4 enables ANSI mode by default, where invalid input, numeric overflow and unparseable dates throw. + * These tests run with ANSI explicitly enabled on the session and materialize the result only after `standardize` + * has returned, as evidence the non-ANSI cast semantics are fixed in the plan at analysis time and do not depend on the + * session setting when the action executes. They also check that overlapping calls on a shared session are safe + * and that the original session setting is restored. + */ +class StandardizationAnsiModeSuite extends AnyFunSuite with SparkTestBase { + + import spark.implicits._ + + private val AnsiKey = "spark.sql.ansi.enabled" + + private val stdConfig = BasicStandardizationConfig + .fromDefault() + .copy(metadataColumns = BasicMetadataColumnsConfig.fromDefault().copy(recordIdStrategy = NoId)) + private implicit val errorCodes: ErrorCodesConfig = stdConfig.errorCodes + + private val schema = StructType(Seq( + StructField("id", StringType, nullable = false), + StructField("num", IntegerType, nullable = true), + StructField("dt", DateType, nullable = true, + new MetadataBuilder().putString(MetadataKeys.Pattern, "yyyy-MM-dd").build()) + )) + + private def source = Seq( + ("1-valid", "42", "2020-01-15"), + ("2-invalid", "abc", "2020-01-15"), + ("3-overflow", "2147483648", "2020-01-15"), + ("4-baddate", "7", "not-a-date") + ).toDF("id", "num", "dt") + + private val expected = Seq( + AnsiRow("1-valid", Some(42), Some(Date.valueOf("2020-01-15")), Seq.empty), + AnsiRow("2-invalid", None, Some(Date.valueOf("2020-01-15")), + Seq(StandardizationErrorMessage.stdCastErr("num", "abc", "string", "integer", None))), + AnsiRow("3-overflow", None, Some(Date.valueOf("2020-01-15")), + Seq(StandardizationErrorMessage.stdCastErr("num", "2147483648", "string", "integer", None))), + AnsiRow("4-baddate", Some(7), None, + Seq(StandardizationErrorMessage.stdCastErr("dt", "not-a-date", "string", "date", Some("yyyy-MM-dd")))) + ) + + private def withAnsiEnabled[T](body: => T): T = { + val previous = spark.conf.getOption(AnsiKey) + spark.conf.set(AnsiKey, "true") + try body + finally previous.fold(spark.conf.unset(AnsiKey))(spark.conf.set(AnsiKey, _)) + } + + private def standardizeAndCollect(): List[AnsiRow] = { + val std = Standardization.standardize(source, schema, stdConfig) + // the action runs after standardize returned, i.e. with the session still in ANSI mode + std.as[AnsiRow].collect().sortBy(_.id).toList + } + + test("invalid values, overflows and unparseable dates become errors, not exceptions, with ANSI mode enabled") { + withAnsiEnabled { + assertResult(expected)(standardizeAndCollect()) + assertResult("true")(spark.conf.get(AnsiKey)) + } + } + + test("overlapping standardizations on a shared session all keep non-ANSI casts and restore the setting") { + withAnsiEnabled { + val threads = 8 + val rounds = 5 + val pool = Executors.newFixedThreadPool(threads) + try { + (1 to rounds).foreach { _ => + val startTogether = new CyclicBarrier(threads) + val tasks = (1 to threads).map(_ => new Callable[List[AnsiRow]] { + override def call(): List[AnsiRow] = { + startTogether.await() + standardizeAndCollect() + } + }) + val results = pool.invokeAll(tasks.asJava).asScala.map(_.get(5, TimeUnit.MINUTES)) + + results.foreach(result => assertResult(expected)(result)) + assertResult("true")(spark.conf.get(AnsiKey)) + } + } finally { + pool.shutdown() + } + } + } +} + +object StandardizationAnsiModeSuite { + case class AnsiRow(id: String, num: Option[Int], dt: Option[Date], errCol: Seq[ErrorMessage]) +} diff --git a/src/test/scala/za/co/absa/standardization/interpreter/stages/TypeParserSuiteTemplate.scala b/src/test/scala/za/co/absa/standardization/interpreter/stages/TypeParserSuiteTemplate.scala index 8336c0f..a57022a 100644 --- a/src/test/scala/za/co/absa/standardization/interpreter/stages/TypeParserSuiteTemplate.scala +++ b/src/test/scala/za/co/absa/standardization/interpreter/stages/TypeParserSuiteTemplate.scala @@ -317,9 +317,9 @@ trait TypeParserSuiteTemplate extends AnyFunSuite with SparkTestBase { } /** - * Spark 4 changed how Column.toString renders expressions compared to Spark 3 and older versions. - * This normalizer converts both expected and actual expressions to the same form , it is only applied for Spark 4. - */ + * Spark 4 changed how `Column.toString` renders expressions compared to Spark 3 and older versions. + * This normalizer converts both expected and actual expressions to the same form , it is only applied for Spark 4. + */ private def normalizeExpr(expr: String): String = { transformPrefixOps(expr) .replace("isNaN(", "isnan(") @@ -332,8 +332,24 @@ trait TypeParserSuiteTemplate extends AnyFunSuite with SparkTestBase { .replaceAll("(\\d)L", "$1") } + /** Spark 4 prefix operators, longer names come first so `isNotNull` is not mistaken for `isNull`, `>=` for `>`. */ private val prefixOps = List("isNotNull", "isNull", "and", "or", "in", ">=", "<=", "=", ">", "<", "/", "%") + /** + * Converts Spark 4 prefix operators to Spark 3 infix syntax while leaving other expressions (e.g. `CAST`, `to_date`, + * `array`, literals) unchanged. + * + * An operator is only recognised at the start of the string or right after `(`, `,` or a space, preventing function + * names that contain operator names from being rewritten. + * + * Examples: + * {{{ + * isNull(x) => (x IS NULL) + * and(isNotNull(a), >(a, 5)) => ((a IS NOT NULL) AND (a > 5)) + * or(isNull(c), !(=(%(d, 1.0), 0.0))) => ((c IS NULL) OR (NOT ((d % 1.0) = 0.0))) + * in(CAST(d AS DOUBLE), Infinity, -Infinity) => (CAST(d AS DOUBLE) IN (Infinity, -Infinity)) + * }}} + */ private def transformPrefixOps(s: String): String = { val sb = new StringBuilder var i = 0 @@ -361,9 +377,11 @@ trait TypeParserSuiteTemplate extends AnyFunSuite with SparkTestBase { sb.toString } + /** Returns the prefix operator starting at position `i` (operator name immediately followed by `(`). */ private def matchOpAt(s: String, i: Int): Option[String] = prefixOps.find(op => s.startsWith(op + "(", i)) + /** Returns the index of the `)` matching the `(` at `openIdx`, e.g. `matchParen("f(a(b))", 1) == 6`. */ private def matchParen(s: String, openIdx: Int): Int = { var depth = 0 var i = openIdx @@ -378,6 +396,7 @@ trait TypeParserSuiteTemplate extends AnyFunSuite with SparkTestBase { throw new IllegalArgumentException(s"Unbalanced parentheses in expression: $s") } + /** Splits a comma separated argument list on top-level commas only, e.g. `"a, f(b, c)"` => `List("a", " f(b, c)")`. */ private def splitArgs(inner: String): List[String] = { val args = scala.collection.mutable.ListBuffer.empty[String] val sb = new StringBuilder @@ -392,6 +411,16 @@ trait TypeParserSuiteTemplate extends AnyFunSuite with SparkTestBase { args.toList } + /** + * Renders one prefix operator with its normalized arguments in Spark 3 infix form: + * {{{ + * renderInfix("isNull", List("x")) => (x IS NULL) + * renderInfix("isNotNull", List("x")) => (x IS NOT NULL) + * renderInfix("and", List("a", "b")) => (a AND b) + * renderInfix("in", List("x", "1", "2")) => (x IN (1, 2)) + * renderInfix(">", List("length(s)", "8")) => (length(s) > 8) + * }}} + */ private def renderInfix(op: String, args: List[String]): String = op match { case "isNull" => s"(${args.head} IS NULL)" case "isNotNull" => s"(${args.head} IS NOT NULL)" From bbdb588fa95fbbe1caccd39976785355d8b43591 Mon Sep 17 00:00:00 2001 From: MatloaItumeleng Date: Tue, 13 Oct 2026 14:43:53 +0200 Subject: [PATCH 8/9] update: adding audit to ci-release to publish all modules --- .github/workflows/release.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9058a5a..2faf4b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,16 +28,32 @@ jobs: fetch-depth: 0 persist-credentials: false + # Spark 4.1 requires Java 17, the Spark 3.5 rows stay Java 11 compatible + # build.sbt pins -release 11 / -source 11 -target 11. - name: Setup JDK uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: distribution: temurin java-version: '17' + cache: sbt + # Making the release auditable: printing every module ci-release will publish, + # ensuring the Spark 4.1 / Scala 2.13 artifact are visible in the log. + # Expected: spark-data-standardization-spark3.5_2.12, + # spark-data-standardization-spark3.5_2.13, + # spark-data-standardization-spark4.1_2.13 + - name: List modules to be published + run: | + sbt -batch -no-colors \ + 'show standardization2_12Spark35 / moduleName' \ + 'show standardizationSpark35 / moduleName' \ + 'show standardizationSpark41 / moduleName' \ + 'show root / publish / skip' - - run: sbt ci-release + - name: Publish all cross-build artifacts + run: sbt ci-release env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} PGP_SECRET: ${{ secrets.PGP_SECRET }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} \ No newline at end of file From a578db7281b773396a0d99b92c9e9e685b5f664a Mon Sep 17 00:00:00 2001 From: MatloaItumeleng Date: Tue, 13 Oct 2026 15:29:19 +0200 Subject: [PATCH 9/9] review comment: new line to release workflow --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2faf4b2..8d96e72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,4 +56,5 @@ jobs: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} PGP_SECRET: ${{ secrets.PGP_SECRET }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} \ No newline at end of file + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + \ No newline at end of file