github-actions[bot] commented on code in PR #69672:
URL: https://github.com/apache/doris/pull/69672#discussion_r4398589131


##########
fe/fe-connector/fe-connector-es/src/main/java/org/apache/doris/connector/es/EsMappingUtils.java:
##########
@@ -150,12 +176,140 @@ public static EsFieldContext 
resolveFieldContext(List<String> columnNames,
             String fieldType = fieldObject.get("type").asText();
             fieldContext.getColumn2typeMap().put(colName, fieldType);
             resolveDateFields(fieldContext, fieldObject, colName, fieldType);
-            resolveKeywordFields(fieldContext, fieldObject, colName, 
fieldType);
             resolveDocValuesFields(fieldContext, fieldObject, colName, 
fieldType);
         }
+        resolveSourceValueSafety(fieldContext, indexMapping, mappingType, 
properties);
         return fieldContext;
     }
 
+    private static void resolveSourceValueSafety(EsFieldContext context, 
String indexMapping, String mappingType,
+            ObjectNode schemaProperties) {
+        try {
+            // Mapping-induced source/index differences on any alias backing 
index must prevent
+            // filtering or replacing source values through that path.
+            List<ObjectNode> roots = new ArrayList<>();
+            Set<String> columns = new 
HashSet<>(context.getColumn2typeMap().keySet());
+            for (JsonNode index : MAPPER.readTree(indexMapping)) {
+                ObjectNode root = getRootSchema((ObjectNode) 
index.get("mappings"), mappingType,
+                        new ArrayList<>(), true);
+                roots.add(root);
+            }
+            // Use the same populated schema as inference, including 
unprojected filter columns.
+            schemaProperties.fieldNames().forEachRemaining(columns::add);
+            // Filters can reference columns absent from the projection. 
Resolve their query paths
+            // from the exposed schema too; the projection still controls 
docvalue read eligibility.
+            for (String column : columns) {
+                ObjectNode field = (ObjectNode) schemaProperties.get(column);
+                resolveKeywordFields(context, field, column, 
field.path("type").asText());
+            }
+            Set<String> unsafeDocValues = new HashSet<>();
+            for (ObjectNode root : roots) {
+                JsonNode properties = root.path("properties");
+                Set<String> copyTargets = new HashSet<>();
+                collectCopyTargets(properties, copyTargets);
+                for (String column : columns) {
+                    JsonNode field = properties.path(column);
+                    JsonNode queryField = fieldAtPath(field, column, 
context.getFetchFieldsContext().get(column));
+                    JsonNode readField = fieldAtPath(field, column, 
context.getDocValueFieldsContext().get(column));
+                    boolean sourceChanged = 
filtersSourceColumn(root.path("_source"), column)

Review Comment:
   [P2] Account for disabled root indexing with populated properties
   
   A root `enabled:false` can retain explicit `properties` and `_source`, while 
ES [skips parsing all user 
fields](https://github.com/elastic/elasticsearch/blob/v8.11.3/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java#L120-L138).
 Map `a` as a keyword with a configured lowercase normalizer and `b` as `long` 
under that root, then insert `{"a":"ok","b":1}`. The new normalizer check 
removes `a`'s docvalue path and forces source reads, but this safety analysis 
never checks the root's `enabled` flag. `SELECT a,b FROM idx WHERE b IS NULL` 
therefore pushes `must_not exists(b)` and prunes its residual: the unindexed 
document matches, yet both readers return `(ok,1)`, violating `IS NULL`. The 
merge base read `(NULL,NULL)` through docvalues for that predicate. The current 
tests only cover disabled roots without properties, where missing-field checks 
hide this case. Unlike the disabled-`_source` thread, source is present here. 
Account for root-disabled indexing on every ba
 cking and retain the affected source predicates locally.



##########
fe/fe-connector/fe-connector-es/src/main/java/org/apache/doris/connector/es/EsScanPlanProvider.java:
##########
@@ -206,15 +211,13 @@ private ScanNodePropertiesResult buildScanNodeProperties(
         // accepts any casing. Passing the raw string through let "TRUE" mean 
https to FE and http to BE.
         nodeProps.put(PROP_HTTP_SSL_ENABLED, 
String.valueOf(props.isHttpSslEnabled()));
 
-        // Query DSL (with not-pushed conjunct tracking)
-        EsQueryDslResult dslResult = buildQueryDsl(filter, state);
-        nodeProps.put(PROP_QUERY_DSL, dslResult.getQueryDsl());
+        // The read mode is scan-wide: losing a's docvalue path also changes a 
keyword sibling b
+        // from scalar docvalues to source JSON (e.g. ["cat"]). Decide it 
before pruning b's predicate.
+        int docValuesMode = useDocValueScan(columns, state);

Review Comment:
   [P2] Preserve integer decoding when switching siblings to source
   
   With docvalues enabled, map `a` as a keyword with a configured lowercase 
normalizer and `b` as ordinary `long`, then insert `{"a":"ok","b":"1.2e3"}`. 
ES's [integer 
parser](https://github.com/elastic/elasticsearch/blob/v8.11.3/libs/x-content/src/main/java/org/elasticsearch/xcontent/support/AbstractXContentParser.java#L147-L182)
 accepts this as 1200. Removing `a`'s path now switches both columns to source, 
where `ScrollParser::insert_int_value` strips everything after the first 
decimal point and decodes `b` as 1. Because ordinary `long` enters neither 
unsafe set, `WHERE b=1200` remains pushed and its residual is pruned, returning 
`(ok,1)`; the merge base and numeric-only docvalue control return 1200. This is 
integer coercion, distinct from the existing scaled/half-float rounding 
threads. The same integer decoder also rejects source `b:[5]` without array 
metadata, although its singleton docvalue previously read successfully. Make 
the newly selected integer source path compatible with
  these supported values, or retain a compatible read path; residuals alone 
cannot repair decoding. Cover both readers and docvalue controls.



##########
fe/fe-connector/fe-connector-es/src/main/java/org/apache/doris/connector/es/EsQueryDslBuilder.java:
##########
@@ -406,17 +455,11 @@ private static String 
buildComparisonDsl(ConnectorComparison.Operator op,
                 // col <=> non-null is equivalent to EQ (term queries exclude 
nulls)
                 return termQuery(column, value);
             case NE:
-                // col != '' means col.length() > 0, NULL should not appear in 
results
-                if (value instanceof String && ((String) value).isEmpty()) {
-                    ObjectNode root = MAPPER.createObjectNode();
-                    ObjectNode boolNode = root.putObject("bool");
-                    boolNode.set("must_not", parseJsonNode(termQuery(column, 
value)));
-                    boolNode.set("must", parseJsonNode(existsQuery(column)));
-                    return root.toString();
-                }
+                // SQL <> is UNKNOWN for missing values, while ES must_not 
alone matches them.
                 ObjectNode neRoot = MAPPER.createObjectNode();
                 ObjectNode neBool = neRoot.putObject("bool");
                 neBool.set("must_not", parseJsonNode(termQuery(column, 
value)));
+                neBool.set("must", parseJsonNode(existsQuery(column)));

Review Comment:
   [P2] Preserve NULL semantics when negating the new inequality DSL
   
   This regresses when `disable_nereids_expression_rules='SIMPLIFY_NOT_EXPR'`. 
With docvalues enabled, `a:long`, `b:keyword`, and `{"a":null,"b":"miss"}`, run 
`SELECT a FROM idx WHERE NOT(a <> 1) OR esquery(b, '{"term":{"b":"hit"}}')`. 
Disabling that rule preserves the reduced scan filter `OR(NOT(NE(a,1)), 
esquery(...))`: `ExpressionTranslator.visitNot` translates the inner negation 
to NE and retains the outer NOT. The new `exists(a)` makes the missing row fail 
the inner NE; `notToDsl` then wraps it in `must_not`, so the outer NOT succeeds 
and the row is returned, although SQL evaluates `UNKNOWN OR false` and excludes 
it. The merge-base DSL excluded this row. Default NOT normalization cancels the 
double negation, so this finding specifically requires the nondefault session 
setting. Normalize the retained negated comparison in this layer, or preserve 
its null rejection without depending on that optional rewrite, and add a 
regression restoring the session setting afterward.



##########
fe/fe-connector/fe-connector-es/src/main/java/org/apache/doris/connector/es/EsMappingUtils.java:
##########
@@ -66,25 +80,37 @@ public static ObjectNode getMapping(String indexMapping) {
      */
     public static ObjectNode getRootSchema(ObjectNode mappings, String 
mappingType,
             List<String> arrayFields) {
+        return getRootSchema(mappings, mappingType, arrayFields, false);
+    }
+
+    private static ObjectNode getRootSchema(ObjectNode mappings, String 
mappingType,
+            List<String> arrayFields, boolean allowEmpty) {
         if (mappingType == null) {
-            checkNonPropertiesFields(mappings, arrayFields);
-            String firstType = mappings.fieldNames().next();
-            if (StreamSupport.stream(Spliterators.spliteratorUnknownSize(
-                    mappings.fieldNames(), Spliterator.ORDERED), false)
-                    .anyMatch(s -> s.contains("properties"))) {
+            checkNonPropertiesFields(mappings, arrayFields, allowEmpty);
+            // Alias safety analysis must accept backing indices that have not 
acquired a mapping yet.
+            if (mappings.isEmpty()) {
                 return mappings;
-            } else {
-                ObjectNode firstData = (ObjectNode) mappings.get(firstType);
-                checkNonPropertiesFields(firstData, arrayFields);
-                return firstData;
             }
+            String firstType = mappings.fieldNames().next();
+            // A typeless mapping may contain only flags or root objects such 
as _source.
+            // Type wrappers are objects; legacy type names may also be names 
of scalar settings.
+            Iterator<String> keys = mappings.fieldNames();
+            while (keys.hasNext()) {
+                String key = keys.next();
+                if (!mappings.get(key).isObject() || 
ROOT_MAPPING_OBJECT_PARAMETERS.contains(key)) {

Review Comment:
   [P2] Preserve legacy mapping types named runtime
   
   `runtime` is a valid ES6 mapping type ([type-name 
validation](https://github.com/elastic/elasticsearch/blob/v6.8.23/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java#L372-L393)).
 For `{"idx":{"mappings":{"runtime":{"properties":{"a":{"type":"long"}}}}}}`, 
this name check returns the outer wrapper instead of its populated type. 
`EsTypeMapping.parseMapping` always calls this parser with a null mapping type, 
so `buildColumns` finds no top-level `properties` and throws `mapping not 
found`; setting `mapping_type=runtime` cannot repair schema inference. The 
merge-base parser unwrapped this response and exposed `a` as BIGINT. This is 
distinct from the empty/configuration-only backing thread: a previously 
supported populated legacy schema is now rejected. Distinguish the wrapper's 
structure from a typeless root-parameter name, and extend the legacy-type tests 
with `runtime`.



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