DanielLeens commented on PR #11493:
URL: https://github.com/apache/seatunnel/pull/11493#issuecomment-5379351698

   # What Problem Does This PR Solve?
   - User pain point: Native semi-structured columns in Snowflake (`VARIANT`), 
Doris (`JSON`/`JSONB`), and StarRocks (`JSON`) were flattened to plain `STRING` 
in SeaTunnel's schema model, so pipelines lost the JSON structure end to end 
and native-JSON sinks received escaped/quoted text instead of a structured 
value.
   - Fix approach: introduce a String-backed `SqlType.JSON` / 
`BasicType.JSON_TYPE` logical type appended at the tail of the existing enum, 
and thread it through the API core, JSON/CSV/Avro formats, 
Calcite/Copy/JsonPath transforms, Spark translation, and the 
Doris/StarRocks/Snowflake catalog, DDL-generation, and write paths, with 
bilingual docs and an incompatible-change entry.
   - One-sentence summary: the type-system design is genuinely sound, but the 
current head (unchanged since 2026-08-14) still has two of its own new E2E 
suites red in CI, and this pass independently confirms a real, 
previously-unformalized *silent data loss* regression in the Qdrant connector 
on top of the already-tracked open findings — this is not ready to merge.
   
   **Housekeeping note up front, since I am reviewing my own PR again on a 
completely unchanged head.** Head `8ddb8ff420f0` has not moved since my last 
full review (submitted the same day, 2026-08-21 14:12:33 UTC). Rather than 
mechanically re-deriving the entire 90-file diff a twelfth time and re-printing 
an unchanged conclusion, this round (a) re-verifies live state, (b) spot-checks 
two of the previously reported "still open" claims directly against source 
rather than trusting my own prior text, and (c) independently traces one new 
angle the prior rounds only mentioned in passing discussion but never 
formalized as a numbered, evidenced Issue: what actually happens, today, when a 
pre-existing first-party sink that has *not* been taught about `JSON` receives 
a field of the new type. I am flagging this explicitly as a deviation from a 
from-scratch re-derivation, with the justification above, per this project's 
review-honesty requirements.
   
   # 1. Code Change Review
   
   ## 1.1 Core Logic Analysis
   
   **Live state re-verified just now, independent of the JSON context 
snapshot:**
   - `headRefOid`: `8ddb8ff420f0ee3e30f7da7f4321d1e6e1f153df` — unchanged since 
2026-08-14.
   - `isDraft`: `true` — still correctly marked Draft.
   - `mergeStateStatus`: `BLOCKED`, `mergeable`: `MERGEABLE`.
   - CI `Build` check: `FAILURE`, `COMPLETED` — same as reported in the last 
full review; no new run has landed for this head.
   - Comment/review counts on the PR are unchanged since my last post (5 issue 
comments, 10 reviews) — nothing has been posted between my last review and this 
one, so there is no unanswered reply to fold in.
   
   **Independent spot-verification of two "still open" carryover claims** 
(re-read from source on the current worktree, not re-asserted from my own prior 
text):
   1. 
`seatunnel-formats/seatunnel-format-text/src/main/java/org/apache/seatunnel/format/text/TextSerializationSchema.java`
 — confirmed no `case JSON:` in the `SqlType` switch (cases present: DOUBLE, 
FLOAT, INT, BOOLEAN, TINYINT, SMALLINT, BIGINT, DECIMAL, STRING, DATE, TIME, 
TIMESTAMP, TIMESTAMP_TZ, NULL, BYTES, ARRAY, MAP, ROW — no JSON). A JSON-typed 
field reaching this path falls to whatever the `default:` branch does. Still 
true.
   2. 
`seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/datatype/DorisTypeConvertorV1Test.java`
 and `DorisTypeConvertorV2Test.java` — confirmed via `git diff 
<merge-base>..HEAD -- <these files>` that both drop 
`Assertions.assertEquals(MAX_STRING_LENGTH, column.getColumnLength())` in the 
same hunk that changes the expected type from `STRING_TYPE` to `JSON_TYPE`, 
with no replacement assertion on what `getColumnLength()` now returns for a 
JSON column. Still true.
   
   **New, independently-verified finding this round: the Qdrant sink silently 
drops JSON-typed fields instead of erroring or storing them.**
   
   I traced the actual field-conversion code, which the prior rounds had only 
referenced by name in discussion, not walked through and evidenced as a formal 
Issue:
   
   ```java
   // QdrantBatchWriter.java (unchanged behavior, exposed by this PR's new 
SqlType.JSON)
   private Points.PointStruct buildPoint(SeaTunnelRow element) {
       ...
       JsonWithInt.Value payloadValue = buildPayload(fieldType, value);
       if (payloadValue != null) {
           point.putPayload(fieldName, payloadValue);
           continue;
       }
       Points.Vector vector = buildVector(fieldType, value);
       if (vector != null) {
           namedVectors.putVectors(fieldName, vector);
       }
       // if both are null, the field is silently omitted from the point 
entirely
   }
   
   public static JsonWithInt.Value buildPayload(SeaTunnelDataType<?> fieldType, 
Object value) {
       switch (fieldType.getSqlType()) {
           case SMALLINT: case INT: case BIGINT: ...
           case STRING: case DATE: return ValueFactory.value(value.toString());
           case BOOLEAN: return 
ValueFactory.value(Boolean.parseBoolean(value.toString()));
           default: return null;   // <-- JSON falls here
       }
   }
   
   public static Points.Vector buildVector(SeaTunnelDataType<?> fieldType, 
Object value) {
       switch (fieldType.getSqlType()) {
           case FLOAT_VECTOR: case FLOAT16_VECTOR: case BFLOAT16_VECTOR: case 
BINARY_VECTOR: ...
           default: return null;   // <-- JSON falls here too
       }
   }
   ```
   
   Before this PR, a Doris/StarRocks/Snowflake JSON-bearing source column was 
reported as `STRING`, so `buildPayload()` matched `case STRING:` and the value 
was written to the point's payload as a plain string. After this PR, the same 
source column is reported as `JSON`, which matches neither switch's explicit 
cases, both helpers return `null`, and `buildPoint()` treats "both null" as 
"not a payload field, not a vector field" — the field is dropped from the 
`PointStruct` entirely, with **no exception, no log line, no metric**. A 
previously-working Doris/StarRocks/Snowflake-source-to-Qdrant-sink pipeline 
that includes a native-JSON column will, after this upgrade, keep running 
successfully and silently write points that are missing that field, forever, 
with no operator-visible signal that anything changed.
   
   This is strictly worse than the already-documented Doris CSV/text gap (Issue 
3 below), which at least throws. Contrast with `connector-selectdb-cloud`'s 
`SeaTunnelRowConverter.convert()`, which I also independently checked and which 
has the same kind of exhaustive switch — but its `default:` branch throws 
`SelectDBConnectorException(UNSUPPORTED_DATA_TYPE, ...)`, so a 
Doris-source-to-SelectDB-Cloud-sink pipeline hard-fails loudly instead of 
silently losing data. Both connectors are outside this PR's own file list 
(neither appears in the 90-file diff), which is exactly the risk category this 
kind of "append an enum constant that other exhaustive switches don't know 
about" change always carries, and it is why I don't think the incompatibility 
surface of this PR is fully mapped yet.
   
   Runtime path (JSON-typed field reaching an un-updated first-party sink):
   ```text
   native source column (Doris JSON|JSONB / StarRocks JSON / Snowflake VARIANT)
     -> catalog + type converter yield BasicType.JSON_TYPE (this PR)
     -> value carried as JSON text; physical class is String.class
     -> sink that has NOT been taught about JSON in this PR's diff:
          connector-selectdb-cloud -> SeaTunnelRowConverter.convert() default: 
throws UNSUPPORTED_DATA_TYPE (fail loud, at least)
          connector-qdrant         -> 
QdrantBatchWriter.buildPayload()/buildVector() default: return null -> field 
silently omitted from the point (fail silent, data loss)
          seatunnel-format-text    -> TextSerializationSchema, no case JSON 
(Issue 3 below, first-party, Doris's own default CSV path)
   ```
   
   ## 1.2 Compatibility Impact
   
   Partially incompatible, intentionally, and documented bilingually — the 
mechanical properties I previously verified still hold on this unchanged head 
(`SqlType.JSON` tail-appended, no ordinal shift, `BasicType.serialVersionUID` 
unchanged, checkpoint/savepoint restore unaffected by the type addition itself, 
old third-party `JdbcDialect` SPI implementations compile unchanged). What 
remains genuinely incomplete is the documented blast radius of the schema 
remap: this round adds one more concretely-verified, unlisted, first-party 
casualty (Qdrant, silent field loss) to the two already tracked (Doris 
CSV/text, unsupported-type throw; SelectDB Cloud, hard-fail per the discussion 
thread, now independently confirmed by me above).
   
   ## 1.3 Performance / Side-Effect Analysis
   
   No new hot-path allocation; unchanged from prior verification. JSON 
parse/validate work is linear in payload size and confined to explicit 
format/protocol boundaries.
   
   ## 1.4 Error Handling and Logging
   
   See Issues 1–2 (CI) and Issue 3 (Qdrant silent drop, new this round) and 
Issue 4 (Doris CSV/text, carryover). No sensitive-data logging introduced.
   
   **Issue 1 (High, carryover, unchanged) — 
`LocalFileIT.testLocalFileCsv`/`testLocalFileJson` both fail with a zero-row 
round trip at the current head**
   - Location: 
`seatunnel-e2e/seatunnel-connector-v2-e2e/connector-file-local-e2e/src/test/java/org/apache/seatunnel/e2e/connector/file/local/LocalFileIT.java:352,393`.
   - Problem: confirmed by the completed fork CI run for the current, 
still-unchanged head 
(`https://github.com/DanielLeens/seatunnel/actions/runs/31789930614`). Both the 
CSV and JSON LocalFile round-trip tests — this PR's own flagship new E2E 
coverage — fail identically with a `MIN_ROW` assertion seeing 0 rows.
   - Potential risk: the PR's documented flagship feature (LocalFile 
round-tripping `json`/`array<json>`) may not actually work end to end.
   - Best improvement: root-cause why the source produces zero rows for this 
exact new fixture; do not merge unexplained.
   - Severity: High. Raised by another reviewer: No.
   
   **Issue 2 (High, carryover, unchanged) — 
`DorisIT.assertJsonColumnsRoundTrip` JSON-null assumption is CI-confirmed 
wrong, contradicting the corrected StarRocks sibling assertion**
   - Location: 
`seatunnel-e2e/seatunnel-connector-v2-e2e/connector-doris-e2e/src/test/java/org/apache/seatunnel/e2e/connector/doris/DorisIT.java:283,295`;
 contrast `connector-starrocks-e2e/.../StarRocksIT.java:526`.
   - Problem: the same completed CI run shows `DorisIT.testDoris` failing 7 
assertions, including the `sawJsonNull` expectation that assumes Doris 
preserves an explicit JSON `null` distinctly from SQL `NULL`; the StarRocks 
sibling test asserts the opposite, empirically-corrected behavior for the same 
scenario.
   - Potential risk: shipping with a known-failing, explicitly-required E2E 
assertion for one of the PR's three named target connectors.
   - Best improvement: verify actual Doris Stream Load JSON-null semantics and 
align the test the same way StarRocks was already corrected.
   - Severity: High. Raised by another reviewer: No.
   
   **Issue 3 (High, new this round, independently verified) — Qdrant sink 
silently drops JSON-typed fields with zero error, log, or metric**
   - Location: 
`seatunnel-connectors-v2/connector-qdrant/src/main/java/org/apache/seatunnel/connectors/seatunnel/qdrant/sink/QdrantBatchWriter.java`
 (`buildPayload()` and `buildVector()` `default:` branches, `buildPoint()`'s 
"both null -> omit field" handling).
   - Problem: verified above — a JSON-typed field (new source-schema behavior 
introduced by this PR) matches neither switch's cases in this pre-existing, 
un-updated connector, both helpers return `null`, and the field is silently 
omitted from the written `PointStruct`. No exception, no log, no metric.
   - Potential risk: a previously-working Doris/StarRocks/Snowflake-to-Qdrant 
pipeline keeps running after upgrade but silently loses a column's data on 
every write, indefinitely, with no operator-visible signal. This is a 
correctness regression, not merely a compatibility break.
   - Best improvement: either (a) make `SqlType.JSON` explicitly rejected 
(throw, matching the SelectDB Cloud pattern) in 
`QdrantBatchWriter.buildPayload()`'s default branch instead of returning 
`null`, since a "no match anywhere -> silently drop the field" behavior is 
itself a latent bug for *any* future enum addition, not just this one, or (b) 
add an explicit `case JSON:` that stores the JSON text as a payload string 
(matching the pre-PR STRING behavior) if that is the intended migration path. 
Either fix also strengthens the case for Issue 5's opt-out ask, since it 
demonstrates the "no opt-out" blast radius is worse than previously documented.
   - Severity: High — silent data loss on an existing, previously-correct 
pipeline is more severe than a documented incompatibility with a clear error 
message. Raised by another reviewer: No (this specific code-level trace and 
default-branch analysis is new this round; the Qdrant connector was named in 
passing in prior discussion-comment threads on this PR, but not previously 
walked through in a numbered, evidenced review Issue).
   
   **Issue 4 (High, carryover, unchanged) — STRING→JSON source-schema remap has 
no opt-out, and the Doris CSV/text gap remains the concrete, reproducible 
instance**
   - Location: `docs/en/introduction/concepts/incompatible-changes.md:27-32`; 
`seatunnel-formats/seatunnel-format-text/.../TextSerializationSchema.java` (no 
`case JSON:`, re-confirmed above); `docs/en/connectors/sink/Doris.md:184`.
   - Problem: a repository-wide grep still returns zero matches for any 
`json.type.mapping`-style opt-out. The Doris CSV/text sink path — a 
previously-working, zero-config pipeline — still throws a generic 
unsupported-type error with no pointer to the documented `CAST(... AS VARCHAR)` 
workaround, and the Doris sink docs still advertise CSV support without the 
JSON caveat. Issue 3 above shows the same class of gap can manifest even more 
dangerously (silently, not loudly) in a different first-party connector.
   - Best improvement: add the missing `case JSON:` pass-through in 
`seatunnel-format-text` (closes the concrete first-party regression) and/or add 
a connector-level opt-out defaulting to legacy `STRING`; correct the Doris sink 
docs either way.
   - Severity: High. Raised by another reviewer: No (carryover from my own 
prior round).
   
   **Issue 5 (Low, blocking by project test-strictness policy, re-verified this 
round) — Three type-converter tests dropped a `getColumnLength()` assertion 
with no replacement**
   - Location: `DorisTypeConvertorV1Test.java:307-308`, 
`DorisTypeConvertorV2Test.java` (equivalent hunk), 
`StarRocksTypeConverterTest.java` (equivalent hunk).
   - Problem: re-confirmed via direct diff inspection this round (see 1.1) — 
the `STRING_TYPE`→`JSON_TYPE` expectation change is correct, but the 
accompanying `getColumnLength()` assertion is deleted with no replacement, so 
nothing pins the intended contract for a JSON column's length going forward.
   - Best improvement: add back an explicit assertion (e.g. 
`assertNull(column.getColumnLength())` if that's the intended contract).
   - Severity: Low, but blocking per this project's policy on unreplaced 
reductions in test strictness. Raised by another reviewer: No.
   
   **Issue 6 (Medium, carryover) — StarRocks CSV/JSON format rejection fires at 
flush time, not at sink initialization**
   - Location: 
`seatunnel-connectors-v2/connector-starrocks/.../serialize/StarRocksBaseSerializer.java:77-80`.
   - Severity: Medium. Raised by another reviewer: Yes (@SEZ9, Issue 3 of their 
2026-08-13 review).
   
   **Issue 7 (Medium, carryover) — Snowflake sink with a user-defined `query` 
silently skips `PARSE_JSON` wrapping**
   - Location: 
`seatunnel-connectors-v2/connector-jdbc/.../dialect/snowflake/SnowflakeDialect.java:50-101`.
   - Severity: Medium. Raised by another reviewer: Yes (@SEZ9, Issue 4 of their 
2026-08-13 review).
   
   **Issue 8 (Low, carryover) — Nested SQL-null/JSON-null distinction lost 
inside `array<json>`**
   - Location: 
`seatunnel-formats/seatunnel-format-json/.../RowToJsonConverters.java:283-291`. 
Severity: Low.
   
   **Issue 9 (Low/Medium, carryover) — `starrocks.config.format` legacy shim 
silently overrides `LOAD_FORMAT` with an unguarded `valueOf`**
   - Location: 
`seatunnel-connectors-v2/connector-starrocks/.../config/SinkConfig.java:97-103`.
 Severity: Low/Medium.
   
   ## 2. Code Quality Assessment
   
   ### 2.1 Coding Standards
   Unchanged from prior verification: title format correct, non-trivial new 
methods generally carry Javadoc. `StarRocksCatalog.getNativeColumnTypes` still 
lacks a comment explaining why it exists (Doris/StarRocks JSON columns report 
as `VARCHAR` over the MySQL wire protocol) — Low/Medium per this project's 
doc-comment standard.
   
   ### 2.2 Test Coverage and Test Stability
   **Mandatory stability rating: Risk present.** Two CI-confirmed, reproducible 
reasons (Issues 1–2), unchanged since the last round. No `Thread.sleep`-based 
waits or floating-point-without-delta assertions found in the JSON-specific 
test code.
   
   ### 2.3 Documentation Updates
   `docs/en` and `docs/zh` remain updated in lockstep with a structured 
incompatible-changes entry. Gap: the Doris CSV/text restriction and the Qdrant 
silent-drop behavior are not reflected anywhere in the incompatible-changes doc 
(Issues 3–4).
   
   # 3. Architectural Soundness
   
   ## 3.1 Elegance of the Solution — Long-term solution
   A String-backed logical type with opt-in, connector-native protocol mapping 
remains the right boundary. What this round adds is evidence that the "surfaces 
that have not opted in fail loudly" assumption (stated in my prior round's 3.3) 
does not hold universally — Qdrant is a first-party counterexample that fails 
silently instead.
   
   ## 3.2 Maintainability
   Unchanged: type mapping, value validation, and protocol-specific 
serialization stay cleanly separated; `JdbcDialect` default-method delegation 
keeps the SPI contract stable.
   
   ## 3.3 Extensibility
   Unchanged assessment, with one correction from this round: I previously 
stated that surfaces without JSON support "fail loudly rather than silently 
mis-stringifying," citing `seatunnel-format-text` as the one exception. That 
statement was incomplete — Qdrant's field-level `null`-means-"no match, skip 
this field" pattern is a second, and worse, exception, and it is a silent 
*drop*, not a mis-stringify.
   
   ## 3.4 Historical-Version Compatibility
   Unchanged from prior verification: `SqlType.JSON` is tail-appended, enum 
serialization is by name, no per-`SqlType` serialization switch in the 
engine/Flink translation path, existing checkpoint/savepoint state restores 
unchanged after upgrade, old third-party `JdbcDialect` SPI implementations 
compile unchanged.
   
   # 4. Issue Summary
   
   | # | Issue | Location | Severity |
   |---|-------|----------|----------|
   | 1 | `LocalFileIT` CSV/JSON round trip both fail with 0 rows — CI-confirmed 
| LocalFileIT.java:352,393 | High |
   | 2 | `DorisIT.assertJsonColumnsRoundTrip` JSON-null assumption CI-confirmed 
wrong | DorisIT.java:283,295 | High |
   | 3 | Qdrant sink silently drops JSON-typed fields, zero error/log/metric 
(new this round) | QdrantBatchWriter.java (`buildPayload`/`buildVector`) | High 
|
   | 4 | STRING→JSON remap has no opt-out; Doris CSV/text is a demonstrated 
zero-config regression | TextSerializationSchema.java; Doris.md:184 | High |
   | 5 | `getColumnLength()` assertions dropped without replacement in three 
converter tests | DorisTypeConvertorV1Test.java:307-308 + siblings | Low 
(blocking by policy) |
   | 6 | StarRocks CSV/JSON format rejection fires at flush, not sink init | 
StarRocksBaseSerializer.java:77-80 | Medium |
   | 7 | Snowflake sink with user-defined `query` skips `PARSE_JSON` | 
SnowflakeDialect.java:50-101 | Medium |
   | 8 | Nested `array<json>` SQL-null/JSON-null distinction lost | 
RowToJsonConverters.java:283-291 | Low |
   | 9 | `starrocks.config.format` shim silently overrides `LOAD_FORMAT` | 
SinkConfig.java:97-103 | Low/Medium |
   
   # 5. Merge Recommendation
   
   ### Conclusion: Not recommended for merge
   
   **1. Blockers — must be fixed**
   1. Issue 1 (High) — root-cause and fix the `LocalFileIT` zero-row failure.
   2. Issue 2 (High) — align `DorisIT`'s JSON-null assumption with actual Doris 
Stream Load behavior.
   3. Issue 3 (High, new) — stop the Qdrant sink from silently dropping 
JSON-typed fields; fail loud at minimum, or add real support.
   4. Issue 4 (High) — close the Doris CSV/text gap and correct the Doris sink 
docs.
   5. Issue 5 (Low, policy-blocking) — restore the dropped `getColumnLength()` 
assertions.
   
   **2. Recommended fixes — non-blocking**
   1. Issue 6 — move the StarRocks CSV/JSON format check to sink initialization.
   2. Issue 7 — fail fast or warn for Snowflake + user-defined `query` + JSON 
field.
   3. Issues 8–9 — document/guard the remaining low-severity gaps.
   
   **Overall assessment.** The type-system design remains sound and every 
mechanical compatibility property I have independently re-verified across 
multiple rounds still holds. What this round changes is the shape of the 
evidence: the CI failures are unchanged and still block merge on their own, and 
the "no opt-out for the schema remap" objection from the discussion thread is 
now backed by a second, independently-traced, code-level example (Qdrant) that 
is strictly worse than the ones already documented, because it fails silently 
rather than loudly. As the PR author, this is not an independent approval — a 
write-capable maintainer must still perform the final review — but from a 
source-correctness standpoint this PR should not move out of Draft until Issues 
1–5 are resolved and a completed CI run on the fixed head is green.
   


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