Skip to content

Commit c92415c

Browse files
committed
Update writer documentation for REDEFINES support and remove noisy warning when multiple REDEFINES are populated; align test field names.
1 parent 97ef938 commit c92415c

3 files changed

Lines changed: 88 additions & 26 deletions

File tree

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,6 +1692,11 @@ val df = spark.read
16921692
.load("/some/path")
16931693
```
16941694
1695+
**Note.** Cobrix uses GPG package from `org.bouncycastle`. Some environments (for example AWS Glue) already include their own
1696+
version of these crypto packages (as part of AWS encryption SDK). This can create binary incompatibilities, so it is recommended
1697+
to shaderelocate this package in your projects: `org.bouncycastle:*` or `org.bouncycastle.**` classes. If you build `spark-cobol`
1698+
using `sbt assembly`, this is already being done.
1699+
16951700
## Summary of all available options
16961701
16971702
##### File reading options
@@ -1838,6 +1843,7 @@ val df = spark
18381843
| .option("write_null_strings_as_spaces", "false") | If 'true' Cobrix will write `null` alphanumeric fields as spaces when writing output files. |
18391844
| .option("write_null_display_numbers_as_zeros", "false") | If 'true' Cobrix will write `null` numeric fields having DISPLAY format as serquence of zeros when writing output files. |
18401845
| .option("write_null_comp3_numbers_as_zeros", "false") | If 'true' Cobrix will write `null` numeric fields having COMP-3 format as zeros when writing EBCDIC files. |
1846+
| .option("write_strict_redefines", "false") | if 'true' Cobrix is going to fail when input DataFrame contains non-null values for more than 1 REDEFINE in a REDEFINE group. |
18411847
18421848
##### Currently supported EBCDIC code pages
18431849
@@ -2010,8 +2016,9 @@ The writer is still in its early stages and has several limitations:
20102016
- `PIC X(n)` alphanumeric.
20112017
- `PIC S9(n)` numeric (integral and decimal) with `DISPLAY`, `COMP`/`COMP-4`/`COMP-5` (big-endian),
20122018
`COMP-1`/`COMP2` (floating-point IBM and IEEE754) , `COMP-3`, and `COMP-9` (Cobrix little-endian).
2013-
- Only fixed record length and variable record length with RDWs are supported (`record_format` is either `F` or `V`).
2014-
- `REDEFINES` are ignored. Cobrix writes only the first field in a REDEFINES group.
2019+
- Record formats (`record_format`) supported: `F`, `V`, `VB`. `records_per_block` or `block_length` needs to be specified for `VB`.
2020+
- `REDEFINE`s are supported. Cobrix writes first non-null REDEFINE in REDEFINE group. It can require for only one REDEFINE
2021+
in a group to be non-null using `write_strict_redefines`.
20152022
- Partitioning by DataFrame fields is not supported.
20162023
20172024
### Implementation details

spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/writer/NestedRecordCombiner.scala

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import org.apache.spark.sql.types.{ArrayType, StructType}
2121
import org.apache.spark.sql.{DataFrame, Row}
2222
import org.slf4j.LoggerFactory
2323
import za.co.absa.cobrix.cobol.parser.Copybook
24-
import za.co.absa.cobrix.cobol.parser.ast.datatype.{AlphaNumeric, COMP3, Decimal, Integral}
24+
import za.co.absa.cobrix.cobol.parser.ast.datatype.{Decimal, Integral}
2525
import za.co.absa.cobrix.cobol.parser.ast.{Group, Primitive, Statement}
2626
import za.co.absa.cobrix.cobol.parser.policies.VariableSizeOccursPolicy
2727
import za.co.absa.cobrix.cobol.parser.recordformats.RecordFormat
@@ -678,11 +678,6 @@ object NestedRecordCombiner {
678678
)
679679
} else {
680680
val chosen = multiple.head
681-
log.warn(
682-
s"Conflicting REDEFINES fields populated on the same row: '$fieldNames'. " +
683-
s"Writing the first populated alternative ('${chosen.fieldName}') and ignoring the rest. " +
684-
s"Set 'write_strict_redefines' to 'true' to fail instead."
685-
)
686681
writeToBytes(chosen.ast, row, ar, currentOffset, variableLengthOccurs, writerParameters)
687682
}
688683
actualSize

spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/writer/FixedLengthEbcdicWriterSuite.scala

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -502,13 +502,13 @@ class FixedLengthEbcdicWriterSuite extends AnyWordSpec with SparkTestBase with B
502502
val copybookContentsWithRedefines =
503503
""" 01 RECORD.
504504
05 A PIC X(1).
505-
05 B PIC 9(5).
506-
05 B1 PIC X(5) REDEFINES B.
505+
05 B1 PIC 9(5).
506+
05 B2 PIC X(5) REDEFINES B1.
507507
"""
508508

509509
"write using only the base field of a REDEFINES group" in {
510510
withTempDirectory("cobol_writer_redefines") { tempDir =>
511-
val df = List(("X", 12345)).toDF("A", "B")
511+
val df = List(("X", 12345)).toDF("A", "B1")
512512

513513
val path = new Path(tempDir, "writer_redefines_base")
514514

@@ -533,7 +533,7 @@ class FixedLengthEbcdicWriterSuite extends AnyWordSpec with SparkTestBase with B
533533

534534
"write using only the redefining field of a REDEFINES group" in {
535535
withTempDirectory("cobol_writer_redefines") { tempDir =>
536-
val df = List(("X", "ABCDE")).toDF("A", "B1")
536+
val df = List(("X", "ABCDE")).toDF("A", "B2")
537537

538538
val path = new Path(tempDir, "writer_redefines_alt")
539539

@@ -558,7 +558,7 @@ class FixedLengthEbcdicWriterSuite extends AnyWordSpec with SparkTestBase with B
558558

559559
"fail fast when both the base and the redefining fields are populated on the same row" in {
560560
withTempDirectory("cobol_writer_redefines") { tempDir =>
561-
val df = List(("X", 12345, "ABCDE")).toDF("A", "B", "B1")
561+
val df = List(("X", 12345, "ABCDE")).toDF("A", "B1", "B2")
562562

563563
val path = new Path(tempDir, "writer_redefines_conflict")
564564

@@ -574,13 +574,13 @@ class FixedLengthEbcdicWriterSuite extends AnyWordSpec with SparkTestBase with B
574574

575575
val messages = causeChainMessages(thrown)
576576
assert(messages.exists(m => m.contains("B") && m.contains("B1")),
577-
s"Expected an error mentioning both conflicting REDEFINES fields 'B' and 'B1', but got: ${messages.mkString(" | ")}")
577+
s"Expected an error mentioning both conflicting REDEFINES fields 'B1' and 'B2', but got: ${messages.mkString(" | ")}")
578578
}
579579
}
580580

581581
"write the first alternative when multiple REDEFINES fields are populated and strict is disabled" in {
582582
withTempDirectory("cobol_writer_redefines") { tempDir =>
583-
val df = List(("X", 12345, "ABCDE")).toDF("A", "B", "B1")
583+
val df = List(("X", 12345, "ABCDE")).toDF("A", "B1", "B2")
584584

585585
val path = new Path(tempDir, "writer_redefines_first_wins")
586586

@@ -610,7 +610,7 @@ class FixedLengthEbcdicWriterSuite extends AnyWordSpec with SparkTestBase with B
610610
("2", None, Some("AAAAA")),
611611
("3", Some(33333), None),
612612
("4", None, Some("BBBBB"))
613-
).toDF("A", "B", "B1")
613+
).toDF("A", "B1", "B2")
614614

615615
val path = new Path(tempDir, "writer_redefines_mixed_rows")
616616

@@ -668,17 +668,77 @@ class FixedLengthEbcdicWriterSuite extends AnyWordSpec with SparkTestBase with B
668668
}
669669
}
670670

671+
"write supporting 2 views on the same field" in {
672+
withTempDirectory("cobol_writer_redefines") { tempDir =>
673+
val copybookContentsWith2Views =
674+
""" 01 RECORD.
675+
05 A PIC X(1).
676+
05 ACCOUNT PIC 9(7).
677+
05 DETAIL REDEFINES ACCOUNT.
678+
10 PREFIX PIC 9(3).
679+
10 SUFFIX PIC 9(4).
680+
"""
681+
682+
// Each row uses one of the 2 views on the same 7 bytes:
683+
// * the flat view - ACCOUNT
684+
// * the split view - DETAIL (PREFIX + SUFFIX)
685+
// * both are present
686+
val exampleJsons = Seq(
687+
"""{"A":"X","ACCOUNT":1234567}""",
688+
"""{"A":"Y","DETAIL":{"PREFIX":234,"SUFFIX":5678}}""",
689+
"""{"A":"Z","ACCOUNT":3456789,"DETAIL":{"PREFIX":456,"SUFFIX":7890}}"""
690+
)
691+
692+
val df = spark.read.json(exampleJsons.toDS())
693+
.select("A", "ACCOUNT", "DETAIL")
694+
695+
val path = new Path(tempDir, "writer_redefines_2_views")
696+
697+
df.coalesce(1)
698+
.orderBy("A")
699+
.write
700+
.format("cobol")
701+
.mode(SaveMode.Overwrite)
702+
.option("copybook_contents", copybookContentsWith2Views)
703+
.save(path.toString)
704+
705+
val bytes = readPartFileBytes(path)
706+
707+
// Row 1: A='X', ACCOUNT=1234567 (DISPLAY digits), DETAIL absent
708+
val row1 = Array[Byte](
709+
0xE7.toByte,
710+
0xF1.toByte, 0xF2.toByte, 0xF3.toByte, 0xF4.toByte, 0xF5.toByte, 0xF6.toByte, 0xF7.toByte
711+
)
712+
713+
// Row 2: A='Y', DETAIL populated (PREFIX=123, SUFFIX=4567), ACCOUNT absent
714+
val row2 = Array[Byte](
715+
0xE8.toByte,
716+
0xF2.toByte, 0xF3.toByte, 0xF4.toByte,
717+
0xF5.toByte, 0xF6.toByte, 0xF7.toByte, 0xF8.toByte
718+
)
719+
720+
// Row 3: A='Z', ACCOUNT=3456789, DETAIL populated (PREFIX=456, SUFFIX=7890)
721+
val row3 = Array[Byte](
722+
0xE9.toByte,
723+
0xF3.toByte, 0xF4.toByte, 0xF5.toByte,
724+
0xF6.toByte, 0xF7.toByte, 0xF8.toByte, 0xF9.toByte
725+
)
726+
727+
assertArraysEqual(bytes, row1 ++ row2 ++ row3)
728+
}
729+
}
730+
671731
"fail fast when two non-adjacent alternatives of a three-way REDEFINES chain are both populated" in {
672732
withTempDirectory("cobol_writer_redefines") { tempDir =>
673733
val copybookContentsWithThreeWayRedefines =
674734
""" 01 RECORD.
675735
05 A PIC X(1).
676-
05 B PIC 9(5).
677-
05 B1 PIC X(5) REDEFINES B.
678-
05 B2 PIC 9(3)V99 REDEFINES B.
736+
05 B1 PIC 9(5).
737+
05 B2 PIC X(5) REDEFINES B1.
738+
05 B3 PIC 9(3)V99 REDEFINES B1.
679739
"""
680740

681-
val df = List(("X", 12345, new java.math.BigDecimal("123.45"))).toDF("A", "B", "B2")
741+
val df = List(("X", 12345, new java.math.BigDecimal("123.45"))).toDF("A", "B1", "B3")
682742

683743
val path = new Path(tempDir, "writer_redefines_third_conflict")
684744

@@ -693,8 +753,8 @@ class FixedLengthEbcdicWriterSuite extends AnyWordSpec with SparkTestBase with B
693753
}
694754

695755
val messages = causeChainMessages(thrown)
696-
assert(messages.exists(m => m.contains("B") && m.contains("B2")),
697-
s"Expected an error mentioning both conflicting REDEFINES fields 'B' and 'B2', but got: ${messages.mkString(" | ")}")
756+
assert(messages.exists(m => m.contains("B1") && m.contains("B3")),
757+
s"Expected an error mentioning both conflicting REDEFINES fields 'B1' and 'B3', but got: ${messages.mkString(" | ")}")
698758
}
699759
}
700760

@@ -703,12 +763,12 @@ class FixedLengthEbcdicWriterSuite extends AnyWordSpec with SparkTestBase with B
703763
val copybookContentsWithThreeWayRedefines =
704764
""" 01 RECORD.
705765
05 A PIC X(1).
706-
05 B PIC 9(5).
707-
05 B1 PIC X(5) REDEFINES B.
708-
05 B2 PIC 9(3)V99 REDEFINES B.
766+
05 B1 PIC 9(5).
767+
05 B2 PIC X(5) REDEFINES B1.
768+
05 B3 PIC 9(3)V99 REDEFINES B1.
709769
"""
710770

711-
val df = List(("X", 12345, new java.math.BigDecimal("123.45"))).toDF("A", "B", "B2")
771+
val df = List(("X", 12345, new java.math.BigDecimal("123.45"))).toDF("A", "B1", "B3")
712772

713773
val path = new Path(tempDir, "writer_redefines_three_first_wins")
714774

0 commit comments

Comments
 (0)