Skip to content

Commit db7096c

Browse files
committed
#420 Fix Hive DDL escaping for complex schema.
1 parent ab9cbf9 commit db7096c

2 files changed

Lines changed: 109 additions & 10 deletions

File tree

‎pramen/core/src/main/scala/za/co/absa/pramen/core/utils/SparkUtils.scala‎

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ object SparkUtils {
208208
* Transforms the schema to the format compatible with Hive-like catalogs.
209209
* - Removes non-nullable flag since it is not compatible with catalogs
210210
* - Switches from string to varchar(n) when the maximum field length is known
211+
*
211212
* @param schema The input schema.
212213
* @return The transformed schema.
213214
*/
@@ -391,43 +392,111 @@ object SparkUtils {
391392
* {{{
392393
* `Id` INT NOT NULL,`Name` STRING,`System Date` ARRAY<STRING>
393394
* }}}
395+
*
394396
* @param sparkDdlExpr An expression as a Spark DDL (df.schema.toDDL)
395397
* @return The same DDL with all column names escaped.
396398
*/
397399
def escapeColumnsSparkDDL(sparkDdlExpr: String): String = {
400+
val STATE_WHITESPACE_OR_ID = 0
401+
val STATE_POSSIBLE_ID = 1
402+
val STATE_DATA_TYPE = 2
403+
val STATE_PARENTHESIS = 3
404+
val STATE_QUOTES = 4
405+
val STATE_ESCAPE = 5
406+
398407
val output = new StringBuilder()
399408
val token = new StringBuilder()
400409

401410
var state = 0
411+
var depth = 0
402412
var i = 0
403413
val len = sparkDdlExpr.length
404414

405415
while (i < len) {
406416
val c = sparkDdlExpr(i)
407-
if (state == 0) {
408-
if (c == ' ' || c == ':') {
409-
state = 1
410-
val tokenStr = token.toString()
411-
if (tokenStr.nonEmpty && tokenStr.head == '`') {
412-
output.append(s"$token$c")
417+
if (state == STATE_WHITESPACE_OR_ID) {
418+
if (c == '<' || c == ':') {
419+
output.append(s"$token")
420+
token.clear()
421+
state = STATE_DATA_TYPE
422+
} else if (c == ' ') {
423+
token.append(c)
424+
} else {
425+
output.append(s"$token")
426+
token.clear()
427+
state = STATE_POSSIBLE_ID
428+
}
429+
}
430+
431+
if (state == 1) {
432+
if (c == '(') {
433+
depth = 1
434+
state = STATE_PARENTHESIS
435+
token.append(c)
436+
} else if (c == '\'') {
437+
state = STATE_QUOTES
438+
token.append(c)
439+
} else if (c == '\\') {
440+
state = STATE_ESCAPE
441+
token.append(c)
442+
} else if (c == ' ' || c == ':') {
443+
state = STATE_DATA_TYPE
444+
val tokenStr = token.toString().trim
445+
if (tokenStr.isEmpty) {
446+
output.append(s"$tokenStr$c")
447+
} else if (tokenStr.head == '`') {
448+
output.append(s"$tokenStr$c")
413449
} else {
414-
output.append(s"`$token`$c")
450+
output.append(s"`$tokenStr`$c")
415451
}
416452
token.clear()
417453
} else if (c == ',' || c == '<') {
418454
output.append(s"$token$c")
419455
token.clear()
456+
state = STATE_WHITESPACE_OR_ID
457+
} else if (c == '>') {
458+
output.append(s"$token$c")
459+
token.clear()
460+
state = STATE_DATA_TYPE
420461
} else {
421462
token.append(c)
422463
}
423-
} else if (state == 1) {
424-
if (c == ',' || c == '<') {
425-
state = 0
464+
} else if (state == STATE_DATA_TYPE) {
465+
if (c == '(') {
466+
depth = 1
467+
state = STATE_PARENTHESIS
468+
} else if (c == '\'') {
469+
state = STATE_QUOTES
470+
} else if (c == '\\') {
471+
state = STATE_ESCAPE
472+
}
473+
if (c == ',') {
474+
state = STATE_WHITESPACE_OR_ID
475+
output.append(s"$token$c")
476+
token.clear()
477+
} else if (c == '<') {
478+
state = STATE_WHITESPACE_OR_ID
426479
output.append(s"$token$c")
427480
token.clear()
428481
} else {
429482
token.append(c)
430483
}
484+
} else if (state == STATE_PARENTHESIS) {
485+
if (c == ')') {
486+
depth -= 1
487+
if (depth == 0)
488+
state = STATE_DATA_TYPE
489+
} else if (c == '(')
490+
depth += 1
491+
token.append(c)
492+
} else if (state == STATE_QUOTES) {
493+
if (c == '\'') {
494+
state = STATE_DATA_TYPE
495+
}
496+
token.append(c)
497+
} else if (state == STATE_ESCAPE) {
498+
state = STATE_DATA_TYPE
499+
token.append(c)
431500
}
432501
i += 1
433502
}

‎pramen/core/src/test/scala/za/co/absa/pramen/core/tests/utils/SparkUtilsSuite.scala‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,36 @@ class SparkUtilsSuite extends AnyWordSpec with SparkTestBase with TempDirFixture
641641

642642
assert(actual == expectedDDL)
643643
}
644+
645+
"work with decimals and nested arrays of struct" in {
646+
val expectedDDL = "`id` BIGINT COMMENT 'This is my table',`legs` ARRAY<STRUCT<`conditions`: ARRAY<STRUCT<`amount`: DECIMAL(18,4), `checks`: ARRAY<STRUCT<`checkNums`: ARRAY<STRING> COMMENT 'decimal(10, 12)'>>>>, `legid`: BIGINT COMMENT 'This is a \\'test\\': long'>>"
647+
648+
val comment1 = new MetadataBuilder().putString("comment", "This is my table").build()
649+
val comment2 = new MetadataBuilder().putString("comment", "decimal(10, 12)").build()
650+
val comment3 = new MetadataBuilder().putString("comment", "This is a 'test': long").build()
651+
val schema = StructType(Array(
652+
StructField("id", LongType, nullable = true, metadata = comment1),
653+
StructField("legs", ArrayType(StructType(List(
654+
StructField("conditions", ArrayType(StructType(List(
655+
StructField("amount", DecimalType(18, 4), nullable = true),
656+
StructField("checks", ArrayType(StructType(List(
657+
StructField("checkNums", ArrayType(StringType, containsNull = true), nullable = true, metadata = comment2)
658+
)), containsNull = true), nullable = true))), containsNull = true), nullable = true),
659+
StructField("legid", LongType, nullable = true, metadata = comment3))), containsNull = true), nullable = true)))
660+
661+
val actualDDL = escapeColumnsSparkDDL(schema.toDDL)
662+
663+
assert(actualDDL == expectedDDL)
664+
}
665+
666+
"work with decimals and nested arrays of struct when the input is not escaped" in {
667+
val inputDDL = "id BIGINT COMMENT 'This is my table',legs ARRAY<STRUCT<conditions: ARRAY<STRUCT<amount: DECIMAL(18,4), checks: ARRAY<STRUCT<checkNums: ARRAY<STRING> COMMENT 'decimal(10, 12)'>>>>, legid: BIGINT COMMENT 'This is a \\'test\\': long'>>"
668+
val expectedDDL = "`id` BIGINT COMMENT 'This is my table',`legs` ARRAY<STRUCT<`conditions`: ARRAY<STRUCT<`amount`: DECIMAL(18,4), `checks`: ARRAY<STRUCT<`checkNums`: ARRAY<STRING> COMMENT 'decimal(10, 12)'>>>>, `legid`: BIGINT COMMENT 'This is a \\'test\\': long'>>"
669+
670+
val actualDDL = escapeColumnsSparkDDL(inputDDL)
671+
672+
assert(actualDDL == expectedDDL)
673+
}
644674
}
645675

646676
}

0 commit comments

Comments
 (0)