Status: Differential correctness verification (not a performance benchmark).
Reference artifact: scratch/external-cobol-test/
Validation date: 2026-09-06
This document records independent external differential correctness evidence for M2C. The goal is to demonstrate that M2C decodes externally generated, mainframe-formatted binary records (CP037 EBCDIC text combined with IBM-style packed decimals) into typed columnar data that semantically matches an industry-standard mainframe connector: AbsaOSS Cobrix on Apache Spark.
This test provides correctness evidence only. It is not a benchmark comparing throughput or latency between M2C and Spark/Cobrix.
- Upstream source: ravi-asati/data-engineering-notes
- Article / Reference:
articles/2025-12-mainframe-for-data-engineers/cobol-to-parquet - Pinned upstream commit / tree:
da33252e98f47a836ba995e43de327b1d7034f3c(branchmain)
- Record synthesis: The upstream Python generator script synthesizes transaction records using pseudo-random values (
random) and system timestamps (datetime.now()). Because values depend on runtime entropy and execution time, re-running the upstream generation script is not byte-deterministic. - Pinned test fixture: The exact input artifact validated here is permanently pinned by:
- Total records: 100
- Record length: 24 bytes
- Total file size: 2,400 bytes
- SHA-256 (
input.ebcdic):a292e8aa6317a247e2fa6091d054449914f21140a46ecfa83d02dff6b0098083The existence of this pinned fixture guarantees exact reproducibility of the validation artifact, even though regeneration from the upstream script does not produce byte-identical data.
- Encoding: Text fields encoded in IBM CP037 EBCDIC; COMP-3 packed decimals in IBM-standard nibble format.
- Output artifact:
TRXN_COBOL_DATA.ebcdic(namedinput.ebcdicin repository tests).
01 TRXN-REC.
05 TRXN-ID PIC 9(9) COMP-3.
05 TRXN-DT PIC X(8).
05 TRXN-TM PIC X(6).
05 TRXN-AMNT PIC S9(7)V99 COMP-3.| Field | Offset | Length | COBOL Clause | Physical Representation |
|---|---|---|---|---|
TRXN-ID |
0 | 5 bytes | PIC 9(9) COMP-3 |
9 digits packed into 5 bytes (unsigned nibbles, terminal sign nibble F) |
TRXN-DT |
5 | 8 bytes | PIC X(8) |
CP037 EBCDIC string (YYYYMMDD) |
TRXN-TM |
13 | 6 bytes | PIC X(6) |
CP037 EBCDIC string (HHMMSS) |
TRXN-AMNT |
19 | 5 bytes | PIC S9(7)V99 COMP-3 |
9 digits (7 integer + 2 fractional), packed into 5 bytes with signed nibble (C/D) |
M2C v0.1 accepts equivalent supported explicit syntax in layout.cpy:
01 TRXN-REC.
05 TRXN-ID PIC 9(9) COMP-3.
05 TRXN-DT PIC X(8).
05 TRXN-TM PIC X(6).
05 TRXN-AMNT PIC S9(7)V9(2) COMP-3.Note
V99 and V9(2) describe identical physical and logical semantics: a 2-digit implicit decimal fraction. M2C's frozen copybook grammar requires the parenthesized count format V9(m). This is a syntactic normalization for M2C v0.1, not a semantic modification.
- Engine:
m2c-pipeline0.1.0 (release build,parquet53,arrow53) - Command:
cargo run --release --bin m2c-pipeline -- convert ` --copybook scratch/external-cobol-test/layout.cpy ` --input scratch/external-cobol-test/input.ebcdic ` --output scratch/external-cobol-test/output.parquet ` --batch-records 100
- Observed Arrow / Parquet Schema:
TRXN-REC.TRXN-ID:decimal128(9, 0)(non-null)TRXN-REC.TRXN-DT:string(non-null)TRXN-REC.TRXN-TM:string(non-null)TRXN-REC.TRXN-AMNT:decimal128(9, 2)(non-null)
- Determinism: The output file has SHA-256
b3190a915e5ec5ea97391edf702558f521970c217c0acbbd8313730c49acfaf8(3,926 bytes).
- Platform: Ubuntu 24.04 LTS on WSL2 (Kernel 5.15.x / 6.x)
- Java Runtime: OpenJDK 17.0.20
- Apache Spark: Spark 4.0.1 (Scala 2.13.x)
- Cobrix Connector:
za.co.absa.cobrix:spark-cobol_2.13:2.9.4 - Upstream Script Configuration:
The upstream
cobol_to_parquet.pyexecutes Spark with:df = spark.read.format("cobol") \ .option("copybook", args.copybook) \ .option("record_length", args.record_length) \ .option("encoding", args.encoding) \ .load(args.input) df.write.mode("overwrite").parquet(args.output)
- Actual Command Invocation:
(Note: Upstream uses default
spark-submit \ --packages za.co.absa.cobrix:spark-cobol_2.13:2.9.4 \ cobol_to_parquet.py \ --input TRXN_COBOL_DATA.ebcdic \ --copybook trxn_writer_simple.cpy \ --record-length 24 \ --encoding EBCDIC \ --output cobrix_trxn_parquet
--encoding EBCDICand specifies--record-length 24; norecord_format=Foption was used). - Observed Spark Schema:
TRXN_ID:integer(int32, nullable)TRXN_DT:string(nullable)TRXN_TM:string(nullable)TRXN_AMNT:decimal(9, 2)(nullable)
Raw Parquet binary files are not expected to match byte-for-byte between independent writers. Parquet writers differ in:
- Writer metadata and version strings (
parquet-mrvsarrow-rs); - Compression settings (Snappy vs uncompressed);
- Row group boundaries and page layout;
- Dictionary encoding choices;
- Metadata ordering and statistics headers.
Correctness is defined as semantic equivalence after standard, well-defined normalizations:
- Column Name Normalization: Strip root/group path prefix (
TRXN-REC.->TRXN-ID) and convert hyphens to underscores (TRXN_ID). - Numeric Representation Normalization:
TRXN_ID: Cobrix represents unscaled COMP-3 with precision 9 asint32, while M2C strictly preserves decimal semantics asDecimal128(9, 0). Both represent integers in range[0, 999,999,999]. Equality is evaluated over integer values (int(m2c_val) == int(cobrix_val)). No floating-point operations are used.TRXN_AMNT: Both engines emitDecimal(9, 2). Decimal values are verified with exact mathematical scale and precision.
- String Fields: Exact string equality after CP037 decode (
TRXN_DTformatYYYYMMDD,TRXN_TMformatHHMMSS). - Key Uniqueness & Ordering:
- Verified that
TRXN_IDhas zero duplicate values in M2C and zero in Cobrix. - Verified that the key sequence is identical without re-indexing.
- Verified that
Validation was performed using scripts/compare_cobrix.py:
python scripts/compare_cobrix.py \
--m2c scratch/external-cobol-test/output.parquet \
--cobrix scratch/external-cobol-test/cobrix_trxn_parquet============================================================
M2C vs Spark/Cobrix Parquet Differential Report
============================================================
M2C original columns: ['TRXN-REC.TRXN-ID', 'TRXN-REC.TRXN-DT', 'TRXN-REC.TRXN-TM', 'TRXN-REC.TRXN-AMNT']
Cobrix original columns: ['TRXN_ID', 'TRXN_DT', 'TRXN_TM', 'TRXN_AMNT']
Normalized columns: ['TRXN_AMNT', 'TRXN_DT', 'TRXN_ID', 'TRXN_TM']
Row count: M2C = 100, Cobrix = 100
Key ordering: IDENTICAL across all 100 records.
Records compared: 100
Field comparison: ALL 4 fields matched across all records.
============================================================
STATUS: PASS (100% semantically identical records)
Sample records decoded by both engines:
TRXN_ID |
TRXN_DT |
TRXN_TM |
TRXN_AMNT |
Match Status |
|---|---|---|---|---|
324638715 |
20260828 |
054238 |
-1575.46 |
EXACT |
324638716 |
20260905 |
065609 |
4662.84 |
EXACT |
324638717 |
20260809 |
034928 |
1379.66 |
EXACT |
324638718 |
20260817 |
041714 |
-665.51 |
EXACT |
324638719 |
20260905 |
130535 |
2672.42 |
EXACT |
| ... (95 more) | ... | ... | ... | EXACT |
Important
This dataset is an externally generated, realistic mainframe-style transaction dataset. It is not a production-extracted mainframe dataset, nor does it prove universal COBOL support (e.g. OCCURS, REDEFINES, or variable-length records remain explicitly unsupported in v0.1).
"M2C successfully processed an externally generated GnuCOBOL fixed-record dataset using CP037 text and COMP-3 numerics. An independent Spark/Cobrix 2.9.4 decode of the same 2,400-byte input produced 100/100 semantically identical logical records after schema normalization."