mattcasters commented on PR #8304:
URL: https://github.com/apache/hop/pull/8304#issuecomment-5616508007

   ## Code Review of PR #8304: Formula Optimization
   
   Great initiative! Bypassing Apache POI's HSSF/XSSF workbook and sheet 
allocation for straightforward expressions is a huge win for one of Hop's most 
heavily used transforms. The 10x–20x microbenchmark improvements demonstrate 
how much overhead POI introduces for row-by-row formula evaluation.
   
   Below are a few critical correctness and runtime failure issues, Excel/POI 
semantic discrepancies, and performance suggestions to address before this can 
safely merge.
   
   ---
   
   ### 1. Critical Bugs & Runtime Failure Risks
   
   #### 1.1 `ArrayIndexOutOfBoundsException` on missing fields and bracketed 
string literals
   In `FastFormulaCompiler`:
   ```java
   // FastFormulaCompiler.key():
   for (String fieldName : fieldNames) {
     key.append('|').append(fieldName);
     
key.append(':').append(rowMeta.getValueMeta(rowMeta.indexOfValue(fieldName)).getType());
   }
   
   // FastFormulaCompiler.eligibleTypes():
   for (String fieldName : fieldNames) {
     int type = rowMeta.getValueMeta(rowMeta.indexOfValue(fieldName)).getType();
     if (!isFastType(type)) { return false; }
   }
   ```
   * **Issue:** If a formula references a field not present in `rowMeta` (e.g. 
typos, missing input columns), `rowMeta.indexOfValue(fieldName)` returns `-1`. 
Calling `rowMeta.getValueMeta(-1)` immediately throws 
`ArrayIndexOutOfBoundsException: Index -1 out of bounds`.
   * **String literal brackets:** In `Formula.java`, `fieldNames` is extracted 
via `FormulaFieldsExtractor.getFormulaFieldList()`, which extracts any text 
inside `[...]` without string literal awareness. A formula like:
     ```excel
     IF([status] = "[ACTIVE]", 1, 0)
     ```
     extracts `"ACTIVE"` as a field name. If `"ACTIVE"` is not a stream field, 
initialization fails with `ArrayIndexOutOfBoundsException: -1` rather than 
falling back to POI.
   * **Recommendation:** Check `int idx = rowMeta.indexOfValue(fieldName); if 
(idx < 0) return CompiledFormula.NOT_ELIGIBLE;` before attempting 
`getValueMeta(idx)`.
   
   ---
   
   #### 1.2 `null` in numeric fields crashes the pipeline with unhandled 
`UnsupportedFormulaException`
   In `FastFormulaEvaluator.toNumber()`:
   ```java
   if (value == null || value == FastFormulaCompiler.NA) {
     throw new UnsupportedFormulaException("Cannot use " + value + " as a 
number");
   }
   ```
   * **Issue:** In Excel and Apache POI, blank/null cells in arithmetic 
expressions (`[qty] * [price]` or `[amount] + 10`) are treated as `0` (`null + 
10 = 10`, `null * 5 = 0`).
   * In `FastFormulaEvaluator`, if an incoming row has a `null` value in a 
numeric field:
     1. `toNumber(null)` throws `UnsupportedFormulaException`.
     2. Because the formula was marked eligible (`fastPath = true`) during 
first row initialization, this exception occurs during row processing 
(`eval(args)`).
     3. `Formula.processRow()` catches `Exception` and diverts to error 
handling or **aborts the pipeline**. There is **no runtime fallback to POI**.
   * Real-world ETL data frequently contains nulls (e.g., from outer joins).
   * **Recommendation:** In `toNumber()`, when `value == null` and `setNa` is 
false, treat `null` as `0.0d` to match Excel/POI arithmetic behavior.
   
   ---
   
   #### 1.3 `FastFormulaCompiler.NA` sentinel leaks into String concatenation 
as `"java.lang.Object@..."`
   In `FastFormulaCompiler`:
   ```java
   public static final Object NA = new Object();
   ```
   And in `FastFormulaEvaluator.TextValue`:
   ```java
   private static String of(Object value) {
     if (value == null) return "";
     if (value instanceof Boolean ...) ...
     if (value instanceof Number ...) ...
     return String.valueOf(value);
   }
   ```
   * **Issue:** When `isSetNa()` is enabled and a null field is encountered, 
`args[i]` is set to `FastFormulaCompiler.NA`. If that field is used in text 
concatenation (`[prefix] & "-" & [comment]`), `TextValue.of(NA)` falls through 
to `String.valueOf(NA)`, resulting in:
     `"prefix-java.lang.Object@4f023fd2"`
   * In Excel/POI, concatenating an `#N/A` error propagates `#N/A` (or produces 
an error cell). It should not output the Java object hash.
   
   ---
   
   #### 1.4 Unchecked exceptions in `compileUncached` escape unhandled
   In `FastFormulaCompiler.compileUncached()`:
   ```java
   try {
     root = FastFormulaEvaluator.parse(resolvedFormula, fieldIndex);
   } catch (UnsupportedFormulaException e) {
     return CompiledFormula.NOT_ELIGIBLE;
   }
   ```
   * **Issue:** If the parser throws an unchecked exception such as 
`NumberFormatException` (e.g., malformed exponential notation `1e-`), 
`StringIndexOutOfBoundsException`, or `NullPointerException`, it bypasses this 
catch block and crashes initialization instead of returning `NOT_ELIGIBLE`.
   * **Recommendation:** Catch `Exception` (or `RuntimeException`) and return 
`CompiledFormula.NOT_ELIGIBLE`.
   
   ---
   
   ### 2. Parity & Semantic Divergences with Excel / Apache POI
   
   1. **`compareEqual` between Boolean and Number / String:**
      ```java
      if (left instanceof Boolean || right instanceof Boolean) {
        return toBoolean(left) == toBoolean(right);
      }
      ```
      * In Excel and POI, booleans and numbers are strictly distinct types: 
`TRUE = 1` is `FALSE`, and `FALSE = 0` is `FALSE`. In `FastFormulaEvaluator`, 
`toBoolean(1)` returns `true`, so `TRUE = 1` evaluates to `TRUE`.
      * Additionally, if `left` is `Boolean` and `right` is `"Y"`, 
`toBoolean("Y")` throws `UnsupportedFormulaException` at runtime instead of 
returning `FALSE`.
   2. **`compareEqual` between Number and String:**
      * In Excel and POI, numbers and strings are never equal (`200 = "200"` is 
`FALSE`). In `FastFormulaEvaluator`, it falls through to 
`TextValue.of(left).equalsIgnoreCase(TextValue.of(right))`, which returns 
`TRUE`.
   3. **Non-standard logical operators (`&&`, `||`, `!`):**
      * Excel formulas use `AND(...)`, `OR(...)`, `NOT(...)`. Excel does not 
support `&&`, `||`, or `!` as logical operators (`!` is the sheet reference 
separator).
      * Introducing `&&` and `||` creates a dialect mismatch: formulas like 
`[a] > 1 && [b] > 1` work on the fast path, but fail with 
`FormulaParseException` in POI if the fast path is disabled or if an 
unsupported function causes fallback.
   4. **`IF` with 2 arguments:**
      * In Excel and POI, the false branch is optional: `=IF([score] >= 60, 
"Pass")` evaluates to `FALSE` when the condition is false. 
`FastFormulaEvaluator` rejects this with `UnsupportedFormulaException("IF 
requires 3 arguments")`.
   5. **`TRIM` whitespace inconsistency:**
      ```java
      if (text.indexOf(' ') < 0) {
        return text.trim();
      }
      ```
      * If there are no spaces, `String.trim()` strips all whitespace (tabs, 
newlines, control characters). If a space exists, the custom loop only 
collapses/strips ASCII spaces (`' '`), leaving tabs and newlines intact.
   
   ---
   
   ### 3. Performance & Memory Optimizations
   
   #### 3.1 Per-row `RowMeta.indexOfValue` lookups and per-row `Object[]` 
allocations
   In `Formula.java`:
   ```java
   private Object[] buildFastArguments(List<String> fieldList, Object[] 
sourceRow, boolean setNa) {
     Object[] args = new Object[fieldList.size()];
     for (int i = 0; i < fieldList.size(); i++) {
       int fieldIndex = data.outputRowMeta.indexOfValue(fieldList.get(i));
       Object value = fieldIndex < 0 ? null : sourceRow[fieldIndex];
       args[i] = (value == null && setNa) ? FastFormulaCompiler.NA : value;
     }
     return args;
   }
   ```
   * `RowMeta.indexOfValue()` acquires a `ReentrantReadWriteLock.readLock()` on 
every call. Calling it for every field of every formula on every single row 
incurs repeated locking and string comparisons.
   * In addition, allocating a new `Object[] args` array per formula per row 
creates avoidable GC churn on large pipelines.
   * **Optimization:** Pre-compute the field indices during `first` into an 
`int[][] fastFieldIndices` array:
     ```java
     int fieldIndex = fastFieldIndices[i][j];
     Object value = sourceRow[fieldIndex];
     ```
   
   #### 3.2 POI resources allocated even when all formulas use the fast path
   In `Formula.java`:
   ```java
   poi = IntStream.range(0, meta.getFormulas().size())
       .mapToObj(it -> new FormulaPoi(this::logDebug))
       .toArray(FormulaPoi[]::new);
   formulaFieldLists = ...;
   ```
   If every formula in the transform is eligible and compiled for the fast 
path, pre-allocating `poi` and extracting `formulaFieldLists` can be skipped.
   
   ---
   
   ### 4. Hop Conventions & Configuration
   
   1. **Hop Variables vs System Properties:**
      * Configuration is currently controlled via JVM property 
`-Dorg.apache.hop.pipeline.transforms.formula.fast.FastFormulaCompiler.enabled=false`.
 In Hop, users configure options via `hop-config.json`, project settings, or 
pipeline execution configurations. Exposing this via Hop environment variables 
or `IVariables` would make it manageable in Hop GUI and `hop-run`.
      * Also, `FastFormulaCompiler.enabled` is cached at class loading 
(`private static volatile boolean enabled = enabledFromProperty();`), meaning 
subsequent calls to `System.setProperty(...)` have no effect unless 
`FastFormulaCompiler.setEnabled()` is called directly.
   2. **Redundant `setNa` in Cache Key:**
      * In `FastFormulaCompiler.key()`:
        ```java
        key.append(setNa ? "na" : "plain").append('=').append(resolvedFormula);
        ```
        `setNa` is not used during AST compilation in `compileUncached`; it is 
only used at runtime in `buildFastArguments()`. Including `setNa` in the cache 
key leads to duplicate cached ASTs.
   


-- 
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]

Reply via email to