fatmanverse commented on PR #12802:
URL: https://github.com/apache/seatunnel/pull/12802#issuecomment-6557065213
Thanks @SEZ9 for the detailed pass. I pushed 8972a5024f for the points
below. I checked a few of them against the EasyExcel 4.0.3 and POI 4.1.2
classes first, because the answer depends on what those libraries do.
**1. Unbounded `BigDecimal` / `toPlainString()`.** Fixed on the sink side.
You are right that the new `toPlainString()` in `ExcelGenerator` is the
dangerous call. It is now `toDecimalText`, which writes the plain text only
when its length (precision + |scale| + sign and point) fits an Excel text cell
(32767 characters) and otherwise writes `BigDecimal#toString()`. So
`1E+2147483647` becomes the cell text `1E+2147483647`
(`ExcelGeneratorTest#testDecimalWithLargeExponentIsWrittenAsScientificText`).
This covers every DECIMAL that reaches the Excel sink, not only those read from
Excel.
I did not add a precision/scale rejection on the read side. `new
BigDecimal(text)` is what the CSV, Text, JSON and XML readers already do for a
DECIMAL column, and before this PR the Excel reader also accepted values that
do not fit the declared type (e.g. `123456789012345678` into `decimal(20, 4)`).
Turning that into a hard failure would be a new behaviour change for Excel
alone. The new scale handling in point 5 is bounded by the declared precision,
so it never expands such a value either (the test reads `1E+2147483647` back
unchanged).
**2. Connector docs.** Added to `docs/{en,zh}/connectors/sink/LocalFile.md`
(after the Excel compression tip): when a DECIMAL becomes a text cell, that
`SUM` skips text cells, and the `CAST` workaround. Added to
`docs/{en,zh}/connectors/source/LocalFile.md` under `excel_engine`: numeric
columns read the stored number (`50%` reads as `0.5`), STRING columns keep the
displayed text, and DECIMAL also reads text cells.
**3. `.xls` with EasyExcel.** I don't think this one happens. In EasyExcel
4.0.3, `NumberRecordHandler` (v03) builds the cell with
`ReadCellData.newInstanceOriginal(BigDecimal.valueOf(record.getValue()), row,
column)`. That sets `originalNumberValue` and then derives `numberValue` from
it by rounding. `FormulaRecordHandler` calls
`setOriginalNumberValue(BigDecimal.valueOf(getValue()))` the same way. RK and
MulRK cells never reach EasyExcel as such: POI's `RecordFactoryInputStream`
converts them to `NumberRecord` first (`RecordFactory.convertToNumberRecord` /
`convertRKRecords`). So `.xls` takes the same fast path as `.xlsx`. To pin it,
I added `testXlsReadKeepsNumericPrecision` for both engines on an `.xls` file
(number cells and a formula cell).
**4. `excel_decimal_mode` option.** I'd rather not add an option in this PR.
Both uniform layouts are already available with the SQL transform before the
sink: `CAST(col AS DOUBLE)` gives number cells (rounded to a double), and
`CAST(col AS STRING)` gives text cells with every digit. The sink doc now says
this. A new option is a permanent contract, so I'd prefer to add it in a
follow-up if users ask for it. Happy to open an issue to track it if you think
it is needed.
**5. Declared scale.** Fixed in `ExcelCellUtils#toDeclaredScale`. A DECIMAL
now gets the declared scale when that is lossless and the result fits the
declared precision, so `5000.0` reads as `5000.0000` in `decimal(20, 4)` from
both number and text cells. I did not use `setScale(scale, HALF_UP)`. A value
with more fractional digits (`1.123456` into `decimal(20, 4)`) is returned
unchanged rather than rounded, as before this PR and as the other format
readers do; silently rounding data on read seemed worse than leaving it to the
sink. The round-trip tests now use `assertEquals` on the exact `BigDecimal`.
`testDecimalReadsWithDeclaredScaleWithoutRounding` covers padding, zero, the
no-rounding case, a value too wide for the precision, and huge exponents
(`1E+2147483647`, and `1000E+2147483647`, whose trailing zeros would underflow
the scale if they were stripped first).
**6. POI FORMULA branch.** Fixed in code rather than in the wording. Looking
at formula results also showed a real problem in the previous head. A computed
value carries binary noise: `(0.1+0.7)*10` is stored as `7.999999999999999` and
`0.1+0.2` as `0.30000000000000004`. With the shortest double text, an INT
column truncated the first to `7`, and a DECIMAL column got the noise. This
already happened with EasyExcel, which reads the full cached formula value, and
with number cells written by other tools. One helper,
`ExcelCellUtils#numberText`, now decides the text of a stored number for a
numeric column, and both engines use it for number cells and numeric formula
results:
- DOUBLE and FLOAT columns get the exact double (`1/3` reads as `1.0 / 3`
with both engines; POI used to return the 15-digit text).
- An integral value keeps all its digits.
- Integer and DECIMAL columns drop computation noise. A value is treated as
noise when it is within 4 ulp of the text Excel shows (`NumberToTextConverter`,
15 significant digits) and that text has at most 13 significant digits, i.e.
the digits Excel dropped were a run of 0s or 9s. So `7.999999999999999` reads
as `8`, `0.30000000000000004` as `0.30`, and `114.99999999999999` (`1.15*100`)
as `115`. A value with 16 or 17 real digits, such as `9.876543210987654`,
`1234567.890123456` or `98765432109876.99`, is not noise and is read exactly,
so BIGINT still truncates rather than rounds. The ulp distance alone was not
enough: for leading digits 8 or 9, many real 16-digit values are within 4 ulp
of their 15-digit text.
The sink uses the same helper to decide number vs text cell, so a DECIMAL
that would not read back unchanged (e.g. `0.30000000000000004`) is written as
text. STRING columns are unchanged. Covered by
`testComputedNumbersReadAsExcelShowsThem` (both engines; formula cells, noisy
number cells, and real 16/17-digit number cells) and
`testXlsReadKeepsNumericPrecision`. The upgrade note and the source doc now
describe this, including formula results.
**7. Hard cast.** Fixed: a non-`BigDecimal` value in a DECIMAL column goes
through `new BigDecimal(value.toString())`, as the old
`Double.parseDouble(value.toString())` path accepted it (covered in the same
`ExcelGeneratorTest` case).
**8. Test comment.** Reworded as you suggested.
Verification, on a JDK 17 container: spotless is clean, and the
`connector-file-base` unit tests pass (391 run, 0 failures, 1 skipped; the two
Excel test classes 32/32). On the previous head bff617912, 7 of the new cases
fail (scale padding, the POI `.xls` formula `1/3`, the noisy INT/DECIMAL reads
and the sink number-vs-text decision); the EasyExcel `.xls` case already passes
there, which matches point 3. The large-exponent sink case crashes the test JVM
there ("too large to fit in a String"). `LocalFileIT#testLocalFileExcel` passes
(6/6).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]