morningman commented on PR #66670:
URL: https://github.com/apache/doris/pull/66670#issuecomment-5475620558

   ## Findings
   
   ### F-01 · `length` is emitted for CHAR/VARCHAR but silently dropped for 
VARBINARY
   
   - **Severity**: Minor
   - **Where**: 
`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/response/SchemaTypeDesc.java:119-122`
   - **Category**: correctness / API completeness
   
   ```java
               case CHAR:
               case VARCHAR:
                   result.length = ((ScalarType) type).getLength();
                   break;
   ```
   
   **What is wrong.** `VARBINARY` is the one other column type whose length 
genuinely varies per column,
   and it is not in this arm, so a `VARBINARY(64)` column reports no `length` 
at all while a
   `VARCHAR(64)` column reports `"length": 64`.
   
   **Why it happens.** `VARBINARY` is a supported column type — 
`fe/fe-type/src/main/java/org/apache/doris/catalog/PrimitiveType.java:55` 
declares
   `VARBINARY("VARBINARY", 16, TPrimitiveType.VARBINARY, true)` — and it 
carries a per-column length:
   `fe/fe-type/src/main/java/org/apache/doris/catalog/ScalarType.java:154` 
dispatches
   `case VARBINARY: return createVarbinaryType(len)`, and `ScalarType.toSql` 
renders `varbinary(N)`.
   The `switch` in `fromType` is an allow-list, so `VARBINARY` falls to 
`default:` and only `kind` and
   `sql` survive.
   
   **When it bites.** A MySQL JDBC catalog with `enable.mapping.varbinary=true` 
maps a remote
   `VARBINARY(64)` through `createVarbinaryType`; a client reading `type_desc` 
for that column gets
   `{"kind":"VARBINARY","sql":"varbinary(64)"}` and must fall back to parsing 
the SQL string — the
   exact failure mode issue #66675 set out to remove. The class's own contract 
at
   
`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/response/SchemaTypeDesc.java:51-52`
 says
   clients should prefer the structured fields.
   
   **Suggested fix.**
   
   ```diff
                case CHAR:
                case VARCHAR:
   +            case VARBINARY:
                    result.length = ((ScalarType) type).getLength();
                    break;
   ```
   
   ### F-02 · VARIANT and AGG_STATE keep their nesting in `sql` but get no 
structure in `type_desc`
   
   - **Severity**: Minor
   - **Where**: 
`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/response/SchemaTypeDesc.java:128-129`
   - **Category**: API completeness
   
   ```java
               default:
                   break;
           }
           return result;
   ```
   
   **What is wrong.** `VARIANT` and `AGG_STATE` are the two remaining column 
types whose `toSql()`
   string carries real nested type structure, and both fall through to 
`default:`, so the structured
   view of a structured type contains no structure at all.
   
   **Why it happens.** `VariantType.toSql` emits
   `variant<<predefined fields...>,PROPERTIES ("variant_max_subcolumns_count" = 
"...", ...)>` — the
   predefined sub-fields are ordinary Doris types with their own names and 
types, and they are dropped.
   `AggStateType.toSql` emits `agg_state<fn(t1 null, t2 not null)>` from its 
`subTypes` and
   `subTypeNullables` lists, also dropped. Neither has a case in the switch.
   
   **When it bites.** A client reading a `VARIANT` column with predefined 
fields gets
   `{"kind":"VARIANT","sql":"variant<...PROPERTIES 
(\"variant_max_subcolumns_count\" = \"0\", ...)>"}`.
   It cannot use the structured view, and the `sql` fallback is hostile to a 
naive parser because the
   `PROPERTIES (...)` clause of internal storage tuning knobs is interleaved 
with the field list.
   
   **Suggested fix.** Either add a `fields` arm for 
`VariantType.getPredefinedFields()` (reusing
   `StructFieldDesc`) and a `sub_types` arm for `AggStateType`, or — if that is 
deliberately out of
   scope for v1 — say so in the class javadoc so a client knows the omission is 
intentional rather than
   an oversight. The former is preferable; the whole point of the field is to 
stop clients parsing SQL
   text.
   
   ### F-03 · `type_sql` can contain the non-SQL sentence `unknown type: 
UNSUPPORTED_TYPE`
   
   - **Severity**: Minor
   - **Where**: 
`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/TableSchemaAction.java:87`
   - **Category**: correctness / contract
   
   ```java
           columnInfo.put("type_sql", colType.toSql());
   ```
   
   **What is wrong.** `type_sql` is documented as "Complete SQL representation 
intended for display and
   compatibility fallback"
   
(`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/response/SchemaTypeDesc.java:51-52`),
 but for
   a column of an unmapped external type it emits an English diagnostic 
sentence, not SQL.
   
   **Why it happens.** `ScalarType.toSql`'s final arm is
   `default: stringBuilder.append("unknown type: ").append(type);`
   
(`fe/fe-type/src/main/java/org/apache/doris/catalog/ScalarType.java:733-735`), 
and
   `fe/fe-type/src/main/java/org/apache/doris/catalog/PrimitiveType.java:33` 
declares `UNSUPPORTED("UNSUPPORTED_TYPE", -1, TPrimitiveType.UNSUPPORTED, 
false)`,
   so the rendered string is literally `unknown type: UNSUPPORTED_TYPE`. It 
lands in both `type_sql`
   and `type_desc.sql`.
   
   **When it bites.** A MySQL JDBC catalog table containing a `GEOMETRY` column:
   `JdbcMySQLClient.jdbcTypeToDoris` falls to `default: return 
Type.UNSUPPORTED;`
   
(`fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/client/JdbcMySQLClient.java:357-358`),
 and
   `JdbcClient.getColumnsFromJdbc`
   
(`fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/client/JdbcClient.java:411-421`)
 adds
   **every** field to the schema without filtering unsupported ones. `GET 
/api/{jdbc_catalog}/{db}/{tbl}/_schema`
   then returns `"type_sql": "unknown type: UNSUPPORTED_TYPE"`. Nothing crashes 
— `Type.UNSUPPORTED` is a
   `ScalarType`, so the switch's `default:` handles it — but a client that 
treats `type_sql` as SQL text
   gets a sentence.
   
   **Suggested fix.** Omit `type_sql`/`sql` when the primitive type is 
`UNSUPPORTED`, so `@JsonInclude(NON_NULL)`
   drops the key and the client sees only `kind: "UNSUPPORTED_TYPE"` — which is 
unambiguous and already
   matches the pre-existing `type` field. Alternatively narrow the javadoc to 
admit the case.
   
   ### F-04 · `type_desc` drops the STRUCT field comment that `sql` keeps
   
   - **Severity**: Minor
   - **Where**: 
`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/response/SchemaTypeDesc.java:143-146`
   - **Category**: API completeness
   
   ```java
       public static class StructFieldDesc {
           private final String name;
           private final boolean containsNull;
           private final SchemaTypeDesc type;
   ```
   
   **What is wrong.** A STRUCT field's comment survives into the `sql` string 
but not into the
   structured view, so for STRUCT types `type_desc` is strictly lossier than 
the string it is supposed
   to replace.
   
   **Why it happens.** `StructField.toSql` appends `comment '<c>'` when a 
comment is set —
   `fe/fe-type/src/main/java/org/apache/doris/catalog/StructField.java:112-114` 
— while `StructFieldDesc`
   captures only `name`, `containsNull` and `type`.
   
   **When it bites.** `STRUCT<a: INT COMMENT 'unit price'>` yields
   `sql: "struct<a:int comment 'unit price'>"` next to
   `fields: [{"name":"a","contains_null":true,"type":{...}}]`. A client that 
follows the class's own
   advice at 
`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/response/SchemaTypeDesc.java:51-52`
 and prefers the structured fields loses the comment; one that
   wants it must parse the SQL string after all.
   
   **Suggested fix.**
   
   ```diff
        public static class StructFieldDesc {
            private final String name;
            private final boolean containsNull;
   +        private final String comment;
            private final SchemaTypeDesc type;
    
            private StructFieldDesc(StructField field) {
                this.name = field.getName();
                this.containsNull = field.getContainsNull();
   +            this.comment = field.isCommentSpecified() ? field.getComment() 
: null;
                this.type = SchemaTypeDesc.fromType(field.getType());
            }
   ```
   
   `@JsonInclude(NON_NULL)` on the class already omits the key when no comment 
is set.
   
   ### F-05 · No test drives a complex type through the real HTTP stack, and 
the wire format is unpinned
   
   - **Severity**: Minor
   - **Where**: 
`fe/fe-core/src/test/java/org/apache/doris/http/TableSchemaActionTest.java:51-54`
   - **Category**: test coverage
   
   ```java
           JSONObject column = (JSONObject) propArray.get(0);
           Assert.assertEquals("BIGINT", column.get("type"));
           Assert.assertEquals("bigint", column.get("type_sql"));
           Assert.assertEquals("BIGINT", ((JSONObject) 
column.get("type_desc")).get("kind"));
   ```
   
   **What is wrong.** Complex types are the entire point of the feature, yet no 
test sends an
   ARRAY/MAP/STRUCT column through the endpoint. Worse, the one assertion that 
does exercise the real
   serialization stack cannot detect a wire-format regression: a BIGINT node 
renders as the
   byte-identical `{"kind":..., "sql":...}` under Jackson-with-annotations, 
Jackson-without-`@JsonNaming`,
   and Gson alike. The snake_case keys that actually matter — `contains_null`, 
`key_contains_null`,
   `value_contains_null`, `element`, `fields` — appear only in unit tests that 
construct their own
   `new ObjectMapper()`, which proves nothing about what Spring Boot's 
converter emits.
   
   **Why it happens.** The only HTTP-level test uses the shared fixture, whose 
columns are just BIGINT
   and DOUBLE 
(`fe/fe-core/src/test/java/org/apache/doris/http/DorisHttpTestCase.java:139-140`),
 and the
   existing end-to-end suite for this exact endpoint,
   `regression-test/suites/http_rest_api/get/test_schema_api.groovy:64-65`, 
still asserts only
   `resultList.size() == 6` and never reads `type_sql` or `type_desc`. The PR's 
own checklist leaves
   "Regression test" unchecked, while root `AGENTS.md` says to prioritise 
regression tests under
   `regression-test/`.
   
   **When it bites.** If `@JsonNaming` were dropped in a later refactor, or 
Spring's converter
   configuration changed, every key of the new object would silently become 
camelCase and CI would stay
   green.
   
   **Suggested fix.** Extend `test_schema_api.groovy` — not the shared FE 
fixture. Adding a
   complex-typed column to `DorisHttpTestCase` would break two unrelated tests 
that assert the fixture
   is exactly `[k1, k2]`
   
(`fe/fe-core/src/test/java/org/apache/doris/datasource/doris/RemoteDorisCompatibleRestClientTest.java:53-59`
   and 
`fe/fe-core/src/test/java/org/apache/doris/datasource/doris/RemoteDorisRestClientTest.java:81-87`).
   The groovy suite already creates its own table: add `ARRAY<DECIMAL(18,4)>`, 
a `MAP`, and a `STRUCT`
   column and assert the snake_case keys and nested values on the live response.
   
   ### F-06 · `assertFalse(node.path(key).asBoolean())` passes when the key is 
absent
   
   - **Severity**: Minor
   - **Where**: 
`fe/fe-core/src/test/java/org/apache/doris/httpv2/rest/response/SchemaTypeDescTest.java:129`
   - **Category**: test coverage
   
   ```java
           JsonNode map = eventsField.path("type").path("element");
           Assert.assertFalse(map.path("key_contains_null").asBoolean());
           Assert.assertTrue(map.path("value_contains_null").asBoolean());
   ```
   
   **What is wrong.** `JsonNode.path()` returns a `MissingNode` for an absent 
key and
   `MissingNode.asBoolean()` returns `false`, so this assertion — and the 
matching one at
   
`fe/fe-core/src/test/java/org/apache/doris/httpv2/rest/response/SchemaTypeDescTest.java:135`
 — passes
   whether the key is present-and-`false` or missing entirely. The negative 
half of the nullability
   contract is therefore not pinned: if `key_contains_null` stopped being 
serialized, the test would
   still be green.
   
   **Why it happens.** `assertFalse(...asBoolean())` conflates "false" with 
"absent". The correct idiom
   is already used in the same file at
   
`fe/fe-core/src/test/java/org/apache/doris/httpv2/rest/response/SchemaTypeDescTest.java:172-173`,
   which uses `json.has(...)`.
   
   **Suggested fix.**
   
   ```diff
   -        Assert.assertFalse(map.path("key_contains_null").asBoolean());
   +        Assert.assertTrue(map.has("key_contains_null"));
   +        Assert.assertFalse(map.get("key_contains_null").asBoolean());
   ```
   
   Apply the same change at line 135 for `contains_null`.
   
   ### F-07 · A user-visible public API change ships with `Release note: None`
   
   - **Severity**: Minor
   - **Where**: 
`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/TableSchemaAction.java:87-88`
   - **Category**: compatibility / process
   
   ```java
           columnInfo.put("type_sql", colType.toSql());
           columnInfo.put("type_desc", SchemaTypeDesc.fromType(colType));
   ```
   
   **What is wrong.** The PR description ticks "Behavior changed: **Yes** — The 
table schema API
   response includes the additive `type_sql` and `type_desc` fields" and 
simultaneously writes
   "Release note: **None**", and ticks "Does this need documentation? **No**" 
for two new fields on a
   public REST API.
   
   **Why it happens.** Root `AGENTS.md`, Commit Standards rule 4: "The `Release 
note` section must be
   filled in for any user-visible behavior or feature change; write 'None' for 
internal refactoring or
   test-only changes." This is a `[feature]` PR on a public endpoint, so the 
note is required. The
   commit message itself is only the title plus `Signed-off-by:`, with none of 
the PR-template body.
   
   **When it bites.** The release note is the only channel that tells operators 
the response shape
   grew. It also matters more here than for the endpoint's previous additive 
changes (#46557 added
   nullable info, #56919 added `column_uid`/`schema_version`): those added 
string values, whereas this
   is the first time a value under `properties[]` is a nested **object**. A 
consumer that tolerates
   unknown keys is unaffected, but one that models a column as 
`Map<String,String>` breaks on an object
   value where every previous addition was survivable.
   
   **Suggested fix.** Replace "None" with a note naming the two fields and 
stating that they are
   additive and that existing fields are unchanged, and open the corresponding 
`doris-website` PR for
   the schema API page.
   
   ### F-08 · ARRAY `contains_null` is a compile-time constant, and three 
assertions on it can never fail
   
   - **Severity**: Nit
   - **Where**: 
`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/response/SchemaTypeDesc.java:60-63`
   - **Category**: API design / test coverage
   
   ```java
       // ARRAY attributes. containsNull describes element nullability, and 
element is the recursively
       // described item type.
       private Boolean containsNull;
       private SchemaTypeDesc element;
   ```
   
   **What is wrong.** For ARRAY nodes this is advertised as a per-column 
attribute but can never be
   anything but `true`, and the tests that assert it are tautologies.
   
   **Why it happens.** `ArrayType.getContainsNull()` has the body `return 
true;` with the javadoc
   "Always returns `true`. Array elements are always nullable in Doris."
   (`fe/fe-type/src/main/java/org/apache/doris/catalog/ArrayType.java:78-83`), 
and the two-argument
   constructor is `@Deprecated` and explicitly discards its `containsNull` 
argument. The consequence in
   the tests is that 
`fe/fe-core/src/test/java/org/apache/doris/httpv2/rest/response/SchemaTypeDescTest.java:108-110`
   are three assertions on a literal, and the test name
   `testNestedArraysReportNullableElementsAtEveryLevel` overclaims — only its 
fourth assertion, on
   `kind`, can fail. This is not wrong output (Doris array elements really are 
always nullable), but a
   client cannot tell that ARRAY's flag is constant while MAP's genuinely 
varies.
   
   **Suggested fix.** Note in the comment at 
`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/response/SchemaTypeDesc.java:60-61`
 that ARRAY `contains_null` is
   always `true` in Doris and is emitted for symmetry with MAP, and rename the 
test to reflect that it
   checks nested `element` chaining rather than nullability.
   
   ### F-09 · The response repeats the same SQL text at every level, and 
`type_sql` duplicates `type_desc.sql`
   
   - **Severity**: Nit
   - **Where**: 
`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/TableSchemaAction.java:87-88`
   - **Category**: performance
   
   ```java
           columnInfo.put("type_sql", colType.toSql());
           columnInfo.put("type_desc", SchemaTypeDesc.fromType(colType));
   ```
   
   **What is wrong.** `type_sql` is byte-identical to `type_desc.sql`, and 
because every node renders
   its own subtree from depth 0 
(`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/response/SchemaTypeDesc.java:76`,
 `this.sql = type.toSql();`), a type of depth
   *d* ships its leaf's SQL text *d* times. Measured duplication for realistic 
nested types is 5–7×.
   
   **Why it happens.** `buildColumnInfo` runs for every base column **and** for 
every column of every
   materialized index 
(`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/TableSchemaAction.java:162-167`),
   and `getIndexIdToMeta()` re-emits the base index as well.
   
   **When it bites.** Per column the addition is roughly +65 B for BIGINT, +111 
B for
   `DECIMALV3(18,4)`, and +609 B for `ARRAY<STRUCT<3 fields>>`. For a 
500-column table with three
   100-column rollups (1300 `buildColumnInfo` calls) the body grows from ~155 
KiB to ~237 KiB
   (+53%) when all columns are scalar, and to ~928 KiB (+499%) when all are 
complex. The CPU cost is
   O(n·d) with *d* bounded by `Type.MAX_NESTING_DEPTH` = 9
   (`fe/fe-type/src/main/java/org/apache/doris/catalog/Type.java:53`), so this 
is a payload concern, not
   a CPU one — and it is paid by every caller, including
   `RemoteDorisRestClient.isTableExist()`, which downloads the whole body and 
reads only `code`.
   
   **Suggested fix.** Optional, and a deliberate trade: issue #66675 explicitly 
asked for both fields,
   so keeping `type_sql` is defensible as a convenience for clients that never 
descend into
   `type_desc`. If payload matters more, drop `type_sql` and let clients read 
`type_desc.sql`, or emit
   `sql` only on the root node.
   
   ### F-10 · `buildColumnInfo` was widened for a test without 
`@VisibleForTesting`
   
   - **Severity**: Nit
   - **Where**: 
`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/TableSchemaAction.java:74`
   - **Category**: style
   
   ```java
       static Map<String, Object> buildColumnInfo(Column column) {
   ```
   
   **What is wrong.** The method went from `private` to package-private 
`static` solely so
   `TableSchemaActionColumnInfoTest` (which lives in the same package) can call 
it, but nothing records
   that intent. 113 files under `fe/fe-core/src/main` use `@VisibleForTesting` 
for exactly this.
   
   **Suggested fix.** Add `@VisibleForTesting` above the declaration. The 
widening itself is sound — the
   new test needs same-package access and the alternative is testing only 
through HTTP.
   
   ### F-11 · New standalone test files use JUnit 4
   
   - **Severity**: Nit
   - **Where**: 
`fe/fe-core/src/test/java/org/apache/doris/httpv2/rest/response/SchemaTypeDescTest.java:30-31`
   - **Category**: style
   
   ```java
   import org.junit.Assert;
   import org.junit.Test;
   ```
   
   **What is wrong.** Part 4.3 of the repo's `code-review` skill asks for JUnit 
5, and `fe-core` is now
   majority JUnit 5 (910 files vs 410). The `httpv2` tree is mixed, but the 
newest file in the exact
   target package, 
`fe/fe-core/src/test/java/org/apache/doris/httpv2/rest/TSOActionTest.java`, is
   JUnit 5.
   
   **Suggested fix.** Move the two **new standalone** files
   (`SchemaTypeDescTest`, `TableSchemaActionColumnInfoTest`) to JUnit 5. Leave
   `fe/fe-core/src/test/java/org/apache/doris/http/TableSchemaActionTest.java` 
on JUnit 4 — it extends
   the JUnit-4 `DorisHttpTestCase` and cannot be converted in isolation.
   
   ### F-12 · The one in-tree consumer of this endpoint is not wired up 
(pre-existing, informational)
   
   - **Severity**: Nit
   - **Where**: 
`fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/TableSchemaAction.java:88`
   - **Category**: observation — **not a defect of this PR**
   
   ```java
           columnInfo.put("type_desc", SchemaTypeDesc.fromType(colType));
   ```
   
   **What is wrong.** Nothing, in this PR. But it is worth telling the author 
that Doris Catalog
   federation already reads this endpoint and reads it wrongly, and that this 
PR builds precisely the
   metadata that would fix it.
   
   **Why it happens.** `RemoteDorisCompatibleRestClient.getColumns` calls 
`api/{db}/{tbl}/_schema`
   
(`fe/fe-core/src/main/java/org/apache/doris/datasource/doris/RemoteDorisCompatibleRestClient.java:50`),
   and `parseColumn`
   
(`fe/fe-core/src/main/java/org/apache/doris/datasource/doris/RemoteDorisCompatibleRestClient.java:79-111`)
   reads keys `nullable`, `key` and `type_attributes` that this endpoint has 
never emitted — it emits
   `is_nullable` (`"Yes"`/`"No"`), `is_key`, and flat top-level 
`precision`/`scale`. So nullability and
   key-ness always come back `false`, and precision/scale are never applied. It 
then calls
   `ScalarType.createType(typeName)`, which for `"ARRAY"`/`"MAP"`/`"STRUCT"` 
hits
   `default: LOG.warn(...); Preconditions.checkState(false);` in
   `fe/fe-type/src/main/java/org/apache/doris/catalog/ScalarType.java` and 
throws.
   
   **When it bites.** Federating a remote Doris table with any ARRAY/MAP/STRUCT 
column throws today,
   before and after this PR.
   
   **Suggested fix.** Out of scope here — but worth a follow-up issue, and 
`type_desc` is the natural
   input for that fix.


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


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to