This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git


The following commit(s) were added to refs/heads/master by this push:
     new 4fae3c6a65 Some enhancement check for bydbQL (#13949)
4fae3c6a65 is described below

commit 4fae3c6a65ce51d0c66d983244fc3b28de7ba311
Author: mrproliu <[email protected]>
AuthorDate: Wed Jul 15 20:49:50 2026 +0800

    Some enhancement check for bydbQL (#13949)
---
 .../banyandb/BanyanDBAggregationQueryDAO.java      |  5 +-
 .../banyandb/stream/AbstractBanyanDBDAO.java       | 25 +--------
 .../storage/plugin/banyandb/stream/Conditions.java | 64 +++++++++++++++++-----
 .../plugin/banyandb/stream/ConditionsTest.java     | 64 ++++++++++++++++++++--
 4 files changed, 117 insertions(+), 41 deletions(-)

diff --git 
a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/BanyanDBAggregationQueryDAO.java
 
b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/BanyanDBAggregationQueryDAO.java
index 0f24223a44..5b9d8c9afe 100644
--- 
a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/BanyanDBAggregationQueryDAO.java
+++ 
b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/BanyanDBAggregationQueryDAO.java
@@ -147,8 +147,9 @@ public class BanyanDBAggregationQueryDAO extends 
AbstractBanyanDBDAO implements
         }
         // Ad-hoc TopN over the raw measure: MEAN aggregation + TOP N + GROUP 
BY entity_id, expressed as a
         // BydbQL `SELECT TOP ? "value" <dir>, MEAN("value"), entity_id ... 
GROUP BY entity_id, "value"`. The
-        // trailing GROUP BY field is inert server-side (grouping uses the tag 
projection, aggregation the Agg
-        // field), so this matches the former typed meanBy(value,{entity_id}) 
+ topN/bottomN(N,value) path.
+        // trailing GROUP BY field is validated and carried on the request but 
does not change the grouping
+        // result (grouping keys come from the tag projection = entity_id; the 
field feeds the MEAN aggregate),
+        // so this matches the former typed meanBy(value,{entity_id}) + 
topN/bottomN(N,value) path.
         final Conditions where = Conditions.create();
         if (CollectionUtils.isNotEmpty(additionalConditions)) {
             additionalConditions.forEach(c -> where.eq(qualify(c.getKey()), 
c.getValue()));
diff --git 
a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/AbstractBanyanDBDAO.java
 
b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/AbstractBanyanDBDAO.java
index 245cb6bcdb..cffa0eb5ab 100644
--- 
a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/AbstractBanyanDBDAO.java
+++ 
b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/AbstractBanyanDBDAO.java
@@ -97,7 +97,7 @@ public abstract class AbstractBanyanDBDAO extends 
AbstractDAO<BanyanDBStorageCli
                 ql.append(" ON 
").append(BanyanDBStorageConfig.StageName.cold.name()).append(" STAGES");
             }
             ql.append(" TIME BETWEEN ? AND ?");
-            appendBodyWithTrace(ql, where.buildQl(), debug);
+            ql.append(where.buildQl(debug));
             final List<Serializable<BanyandbModel.TagValue>> params = 
timeBoundedParams(timestampRange, where.params());
             final StreamQueryResponse response =
                 getClient().queryStream(ql.toString(), params.toArray(new 
Serializable[0]));
@@ -306,7 +306,7 @@ public abstract class AbstractBanyanDBDAO extends 
AbstractDAO<BanyanDBStorageCli
                 ql.append(" ON 
").append(BanyanDBStorageConfig.StageName.cold.name()).append(" STAGES");
             }
             ql.append(" TIME BETWEEN ? AND ?");
-            appendBodyWithTrace(ql, where.buildQl(), debug);
+            ql.append(where.buildQl(debug));
             final List<Serializable<BanyandbModel.TagValue>> params = 
timeBoundedParams(timestampRange, where.params());
             final MeasureQueryResponse response =
                 getClient().queryMeasure(ql.toString(), params.toArray(new 
Serializable[0]));
@@ -361,25 +361,6 @@ public abstract class AbstractBanyanDBDAO extends 
AbstractDAO<BanyanDBStorageCli
         return params;
     }
 
-    /**
-     * Append the clause tail, inserting {@code WITH QUERY_TRACE} (when 
debugging) before any
-     * {@code LIMIT}/{@code OFFSET} — BydbQL requires the trace marker ahead 
of LIMIT/OFFSET or the parser
-     * rejects it with {@code unexpected token "WITH"}.
-     */
-    private static void appendBodyWithTrace(StringBuilder ql, String body, 
boolean debug) {
-        final String tail = body == null ? "" : body;
-        if (!debug) {
-            ql.append(tail);
-            return;
-        }
-        final int limitIdx = tail.indexOf(" LIMIT ");
-        if (limitIdx >= 0) {
-            ql.append(tail, 0, limitIdx).append(" WITH 
QUERY_TRACE").append(tail, limitIdx, tail.length());
-        } else {
-            ql.append(tail).append(" WITH QUERY_TRACE");
-        }
-    }
-
     /**
      * Double-quote a column identifier for BydbQL. Quoting unconditionally 
sidesteps the reserved-keyword
      * check (e.g. a column named {@code count}) without tracking BanyanDB's 
keyword list.
@@ -437,7 +418,7 @@ public abstract class AbstractBanyanDBDAO extends 
AbstractDAO<BanyanDBStorageCli
                 ql.append(" ON 
").append(BanyanDBStorageConfig.StageName.cold.name()).append(" STAGES");
             }
             ql.append(" TIME BETWEEN ? AND ?");
-            appendBodyWithTrace(ql, where.buildQl(), debug);
+            ql.append(where.buildQl(debug));
             final List<Serializable<BanyandbModel.TagValue>> params = 
timeBoundedParams(timestampRange, where.params());
             final TraceQueryResponse response =
                 getClient().queryTrace(ql.toString(), params.toArray(new 
Serializable[0]));
diff --git 
a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/Conditions.java
 
b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/Conditions.java
index bee77a96df..fe7a8beaa1 100644
--- 
a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/Conditions.java
+++ 
b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/Conditions.java
@@ -36,6 +36,12 @@ public final class Conditions {
     private final StringBuilder ql = new StringBuilder();
     private final List<Serializable<BanyandbModel.TagValue>> params = new 
ArrayList<>();
     private final boolean groupMode;
+    /**
+     * Offset in {@link #ql} where the {@code LIMIT}/{@code OFFSET} tail 
begins, or {@code -1} if there is none.
+     * Recorded when the first of {@code limit()}/{@code offset()} is appended 
so {@link #buildQl(boolean)} can
+     * position {@code WITH QUERY_TRACE} ahead of pagination without scanning 
the generated text.
+     */
+    private int paginationStart = -1;
 
     private Conditions(boolean groupMode) {
         this.groupMode = groupMode;
@@ -84,6 +90,9 @@ public final class Conditions {
     }
 
     public Conditions in(String column, List<String> values) {
+        if (values == null || values.isEmpty()) {
+            throw new IllegalArgumentException("IN values must not be empty 
for column: " + column);
+        }
         return condition(column, " IN (?)", Value.stringArrayTagValue(values));
     }
 
@@ -106,6 +115,9 @@ public final class Conditions {
     }
 
     public Conditions having(String column, List<String> values) {
+        if (values == null || values.isEmpty()) {
+            throw new IllegalArgumentException("HAVING values must not be 
empty for column: " + column);
+        }
         return condition(column, " HAVING (?)", 
Value.stringArrayTagValue(values));
     }
 
@@ -149,12 +161,18 @@ public final class Conditions {
     }
 
     public Conditions limit(long value) {
+        if (paginationStart < 0) {
+            paginationStart = ql.length();
+        }
         ql.append(" LIMIT ?");
         params.add(Value.longTagValue(value));
         return this;
     }
 
     public Conditions offset(long value) {
+        if (paginationStart < 0) {
+            paginationStart = ql.length();
+        }
         ql.append(" OFFSET ?");
         params.add(Value.longTagValue(value));
         return this;
@@ -168,6 +186,23 @@ public final class Conditions {
         return ql.toString();
     }
 
+    /**
+     * The QL body with the {@code WITH QUERY_TRACE} debug marker optionally 
injected at the grammar-mandated
+     * position — after {@code ORDER BY}, before any {@code LIMIT}/{@code 
OFFSET}. The position comes from the
+     * recorded {@link #paginationStart}, so no scanning of the generated QL 
text is required (robust even if a
+     * column name or projection contained {@code " LIMIT "}, and correct for 
an {@code OFFSET}-only body).
+     *
+     * @param withQueryTrace whether to inject {@code WITH QUERY_TRACE}
+     * @return the QL body, with the trace marker inserted ahead of pagination 
when requested
+     */
+    public String buildQl(boolean withQueryTrace) {
+        if (!withQueryTrace) {
+            return ql.toString();
+        }
+        final int pos = paginationStart < 0 ? ql.length() : paginationStart;
+        return ql.substring(0, pos) + " WITH QUERY_TRACE" + ql.substring(pos);
+    }
+
     /**
      * @return the bound parameters, in the order their {@code ?} placeholders 
appear in {@link #buildQl()}.
      */
@@ -176,9 +211,11 @@ public final class Conditions {
     }
 
     /**
-     * Combine condition {@link #group() groups} with OR: {@code WHERE (g1) OR 
(g2) OR ...}. A single non-empty
-     * group is emitted without parentheses ({@code WHERE g1}); empty groups 
are skipped; if every group is empty
-     * no clause is emitted. Params are appended in group order.
+     * Combine condition {@link #group() groups} with OR, wrapping the whole 
disjunction in an outer paren so it
+     * composes correctly with any surrounding predicate: {@code WHERE ((g1) 
OR (g2) OR ...)}, or {@code WHERE (g1)}
+     * for a single group. The outer paren is what makes {@code 
.eq(x).or(groups)} bind as
+     * {@code x AND ((g1) OR (g2))} rather than the precedence-wrong {@code x 
AND g1 OR g2}. Empty groups are
+     * skipped; if every group is empty no clause is emitted. Params are 
appended in group order.
      *
      * @param groups the condition groups, each built via {@link #group()}
      * @return this builder
@@ -193,19 +230,20 @@ public final class Conditions {
         if (nonEmpty.isEmpty()) {
             return this;
         }
-        ql.append(ql.length() == 0 ? " WHERE " : " AND ");
-        if (nonEmpty.size() == 1) {
-            ql.append(nonEmpty.get(0).ql);
-            params.addAll(nonEmpty.get(0).params);
-        } else {
-            for (int i = 0; i < nonEmpty.size(); i++) {
-                if (i > 0) {
-                    ql.append(" OR ");
-                }
+        final boolean multi = nonEmpty.size() > 1;
+        ql.append(ql.length() == 0 ? " WHERE " : " AND ").append("(");
+        for (int i = 0; i < nonEmpty.size(); i++) {
+            if (i > 0) {
+                ql.append(" OR ");
+            }
+            if (multi) {
                 ql.append("(").append(nonEmpty.get(i).ql).append(")");
-                params.addAll(nonEmpty.get(i).params);
+            } else {
+                ql.append(nonEmpty.get(i).ql);
             }
+            params.addAll(nonEmpty.get(i).params);
         }
+        ql.append(")");
         return this;
     }
 
diff --git 
a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/ConditionsTest.java
 
b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/ConditionsTest.java
index 25f71ab1ff..f8ec24a82c 100644
--- 
a/oap-server/server-storage-plugin/storage-banyandb-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/ConditionsTest.java
+++ 
b/oap-server/server-storage-plugin/storage-banyandb-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/banyandb/stream/ConditionsTest.java
@@ -24,6 +24,7 @@ import 
org.apache.skywalking.library.banyandb.v1.client.metadata.Serializable;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class ConditionsTest {
@@ -129,10 +130,10 @@ public class ConditionsTest {
     }
 
     @Test
-    public void orWithSingleNonEmptyGroupHasNoParentheses() {
+    public void orWithSingleGroupIsParenthesized() {
         final Conditions where = Conditions.create().or(List.of(
                 Conditions.group().in("a", List.of("x"))));
-        assertEquals(" WHERE a IN (?)", where.buildQl());
+        assertEquals(" WHERE (a IN (?))", where.buildQl());
         assertEquals(1, where.params().size());
     }
 
@@ -141,7 +142,7 @@ public class ConditionsTest {
         final Conditions where = Conditions.create().or(List.of(
                 Conditions.group().eq("a", "x"),
                 Conditions.group().eq("b", "y")));
-        assertEquals(" WHERE (a = ?) OR (b = ?)", where.buildQl());
+        assertEquals(" WHERE ((a = ?) OR (b = ?))", where.buildQl());
         final List<Serializable<BanyandbModel.TagValue>> params = 
where.params();
         assertEquals(2, params.size());
         assertEquals("x", str(params.get(0)));
@@ -154,7 +155,62 @@ public class ConditionsTest {
                 Conditions.group(),
                 Conditions.group().eq("a", "x"),
                 Conditions.group()));
-        assertEquals(" WHERE a = ?", where.buildQl());
+        assertEquals(" WHERE (a = ?)", where.buildQl());
         assertEquals(1, where.params().size());
     }
+
+    @Test
+    public void orAfterExistingPredicateIsParenthesizedForCorrectPrecedence() {
+        final Conditions where = Conditions.create()
+                .eq("tenant", "t")
+                .or(List.of(
+                        Conditions.group().eq("a", "x"),
+                        Conditions.group().eq("b", "y")));
+        // The outer parens are what keep AND from binding tighter than OR:
+        // tenant = ? AND ((a = ?) OR (b = ?)), not (tenant = ? AND a = ?) OR 
b = ?.
+        assertEquals(" WHERE tenant = ? AND ((a = ?) OR (b = ?))", 
where.buildQl());
+        assertEquals(3, where.params().size());
+    }
+
+    @Test
+    public void buildQlWithTraceInsertsBeforeLimitAndOffset() {
+        final Conditions where = Conditions.create().eq("a", 
"x").orderByDesc("t").limit(10).offset(20);
+        assertEquals(" WHERE a = ? ORDER BY t DESC WITH QUERY_TRACE LIMIT ? 
OFFSET ?", where.buildQl(true));
+    }
+
+    @Test
+    public void buildQlWithTraceInsertsBeforeOffsetOnly() {
+        final Conditions where = Conditions.create().eq("a", "x").offset(20);
+        assertEquals(" WHERE a = ? WITH QUERY_TRACE OFFSET ?", 
where.buildQl(true));
+    }
+
+    @Test
+    public void buildQlWithTraceAppendsAtEndWhenNoPagination() {
+        final Conditions where = Conditions.create().eq("a", 
"x").orderByDesc("t");
+        assertEquals(" WHERE a = ? ORDER BY t DESC WITH QUERY_TRACE", 
where.buildQl(true));
+    }
+
+    @Test
+    public void buildQlWithTraceFalseEqualsPlainBuildQl() {
+        final Conditions where = Conditions.create().eq("a", "x").limit(5);
+        assertEquals(where.buildQl(), where.buildQl(false));
+    }
+
+    @Test
+    public void emptyInValuesAreRejectedLocally() {
+        assertThrows(IllegalArgumentException.class, () -> 
Conditions.create().in("a", List.of()));
+    }
+
+    @Test
+    public void emptyHavingValuesAreRejectedLocally() {
+        assertThrows(IllegalArgumentException.class, () -> 
Conditions.create().having("a", List.of()));
+    }
+
+    @Test
+    public void valuesAreBoundAsParamsNotInterpolatedIntoQl() {
+        // An injection-looking value stays a bound parameter (data); it never 
becomes part of the QL text.
+        final Conditions where = Conditions.create().eq("a", "' OR 1=1 --");
+        assertEquals(" WHERE a = ?", where.buildQl());
+        assertEquals("' OR 1=1 --", str(where.params().get(0)));
+    }
 }

Reply via email to