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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new e143e48790f1 CAMEL-24574: camel-google-bigquery - document ${name} as 
literal substitution and warn when it is not an identifier
e143e48790f1 is described below

commit e143e48790f15a308240a348817b64915e43180d
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Sep 1 17:04:31 2026 +0200

    CAMEL-24574: camel-google-bigquery - document ${name} as literal 
substitution and warn when it is not an identifier
    
    The google-bigquery-sql endpoint accepts two placeholder forms that behave
    differently: @name is bound as a named BigQuery query parameter, while
    ${name} is spliced as literal text into the query before it is sent (needed
    because BigQuery parameters can bind values but not identifiers). The docs
    only described the ${name} form and didn't convey this split, so a reader
    could reasonably use ${name} for values -- a SQL injection risk if that
    value comes from untrusted input.
    
    Adds a "Query Placeholders" doc section contrasting the two forms and
    warning that ${name} must never be populated from untrusted message
    content. SqlHelper.translateQuery now logs a WARN (name only, never the
    value) when a ${name} substitution doesn't look like a BigQuery identifier.
    No behavior change to the substitution itself.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    Closes #26007
---
 .../docs/google-bigquery-sql-component.adoc        | 57 ++++++++++++++++++++--
 .../main/docs/google-bigquery-sql-component.adoc   | 57 ++++++++++++++++++++--
 .../component/google/bigquery/sql/SqlHelper.java   | 37 ++++++++++++++
 .../bigquery/sql/SqlHelperIdentifierTest.java      | 41 ++++++++++++++++
 .../google/bigquery/unit/sql/SqlHelperTest.java    | 11 +++++
 5 files changed, 195 insertions(+), 8 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-bigquery-sql-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-bigquery-sql-component.adoc
index 5c8ca0d2b0d8..21e99864da06 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-bigquery-sql-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-bigquery-sql-component.adoc
@@ -82,10 +82,8 @@ google-bigquery-sql://project-17248459:delete * from 
test.table where id=@myId
 
 google-bigquery-sql://project-17248459:delete * from ${datasetId}.${tableId} 
where id=@myId
 ----
-where
-
- * parameters in form $\{name} are extracted from message headers and formed 
the translated query.
- * parameters in form @name are extracted from body or message headers and 
sent to Google Bigquery. The `com.google.cloud.bigquery.StandardSQLTypeName` of 
the parameter is detected from the type of the parameter using `<T> 
QueryParameterValue<T>.of(T value, Class<T> type)`
+The query can contain two kinds of placeholder, `@name` and `$\{name}`. They 
are not
+interchangeable: see xref:#_query_placeholders[Query Placeholders] below.
 
 You can externalize your SQL queries to files in the classpath or file system 
as shown:
 
@@ -104,6 +102,57 @@ include::partial$component-endpoint-headers.adoc[]
 
 Google BigQuery SQL endpoint expects the payload to be either empty or a map 
of query parameters.
 
+== Query Placeholders
+
+A query may contain two kinds of placeholder. They cover different positions 
in a SQL
+statement and are not interchangeable.
+
+[cols="1,2,3",options="header"]
+|===
+|Form |Substitutes |Mechanism
+
+|`@name`
+|Values
+|Sent to BigQuery as a named query parameter, separately from the query text.
+
+|`$\{name}`
+|Identifiers, such as dataset and table names
+|Spliced into the query text as literal characters before the query is sent.
+|===
+
+=== @name - query parameters
+
+Use `@name` for every value.
+
+Parameters in the form `@name` are taken from the message body when it is a 
`Map`,
+otherwise from the message headers, and are bound as BigQuery named query 
parameters.
+The `com.google.cloud.bigquery.StandardSQLTypeName` of the parameter is 
detected from
+the Java type of the value using `<T> QueryParameterValue<T>.of(T value, 
Class<T> type)`.
+
+Because the value is sent to BigQuery separately from the query text, it 
cannot alter
+the structure of the query. It is always treated as data.
+
+=== $\{name} - literal identifier substitution
+
+Parameters in the form `$\{name}` are replaced with the `String` value of the 
matching
+message header, or of the exchange property when no such header exists, before 
the query
+is sent. If neither is present, the exchange fails with a 
`RuntimeExchangeException`.
+
+The substitution is a literal text splice: the value is inserted into the 
query exactly
+as it appears, with no quoting or escaping applied. This form exists because 
BigQuery
+named query parameters can bind values but cannot bind identifiers, so a 
dataset or table
+name cannot be supplied through `@name`:
+
+----
+google-bigquery-sql://project-17248459:delete * from ${datasetId}.${tableId} 
where id=@myId
+----
+
+IMPORTANT: `$\{name}` is not parameter binding. Use it only for identifiers 
such as dataset
+and table names, and only with values that your route controls. A value 
containing SQL
+syntax changes the structure of the executed query instead of being treated as 
data, so a
+`$\{name}` placeholder must never be populated from message content supplied 
by an untrusted
+sender. Use `@name` for values.
+
 == Query Types
 
 The component supports both SELECT and DML (INSERT, UPDATE, DELETE) queries:
diff --git 
a/components/camel-google/camel-google-bigquery/src/main/docs/google-bigquery-sql-component.adoc
 
b/components/camel-google/camel-google-bigquery/src/main/docs/google-bigquery-sql-component.adoc
index 5c8ca0d2b0d8..21e99864da06 100644
--- 
a/components/camel-google/camel-google-bigquery/src/main/docs/google-bigquery-sql-component.adoc
+++ 
b/components/camel-google/camel-google-bigquery/src/main/docs/google-bigquery-sql-component.adoc
@@ -82,10 +82,8 @@ google-bigquery-sql://project-17248459:delete * from 
test.table where id=@myId
 
 google-bigquery-sql://project-17248459:delete * from ${datasetId}.${tableId} 
where id=@myId
 ----
-where
-
- * parameters in form $\{name} are extracted from message headers and formed 
the translated query.
- * parameters in form @name are extracted from body or message headers and 
sent to Google Bigquery. The `com.google.cloud.bigquery.StandardSQLTypeName` of 
the parameter is detected from the type of the parameter using `<T> 
QueryParameterValue<T>.of(T value, Class<T> type)`
+The query can contain two kinds of placeholder, `@name` and `$\{name}`. They 
are not
+interchangeable: see xref:#_query_placeholders[Query Placeholders] below.
 
 You can externalize your SQL queries to files in the classpath or file system 
as shown:
 
@@ -104,6 +102,57 @@ include::partial$component-endpoint-headers.adoc[]
 
 Google BigQuery SQL endpoint expects the payload to be either empty or a map 
of query parameters.
 
+== Query Placeholders
+
+A query may contain two kinds of placeholder. They cover different positions 
in a SQL
+statement and are not interchangeable.
+
+[cols="1,2,3",options="header"]
+|===
+|Form |Substitutes |Mechanism
+
+|`@name`
+|Values
+|Sent to BigQuery as a named query parameter, separately from the query text.
+
+|`$\{name}`
+|Identifiers, such as dataset and table names
+|Spliced into the query text as literal characters before the query is sent.
+|===
+
+=== @name - query parameters
+
+Use `@name` for every value.
+
+Parameters in the form `@name` are taken from the message body when it is a 
`Map`,
+otherwise from the message headers, and are bound as BigQuery named query 
parameters.
+The `com.google.cloud.bigquery.StandardSQLTypeName` of the parameter is 
detected from
+the Java type of the value using `<T> QueryParameterValue<T>.of(T value, 
Class<T> type)`.
+
+Because the value is sent to BigQuery separately from the query text, it 
cannot alter
+the structure of the query. It is always treated as data.
+
+=== $\{name} - literal identifier substitution
+
+Parameters in the form `$\{name}` are replaced with the `String` value of the 
matching
+message header, or of the exchange property when no such header exists, before 
the query
+is sent. If neither is present, the exchange fails with a 
`RuntimeExchangeException`.
+
+The substitution is a literal text splice: the value is inserted into the 
query exactly
+as it appears, with no quoting or escaping applied. This form exists because 
BigQuery
+named query parameters can bind values but cannot bind identifiers, so a 
dataset or table
+name cannot be supplied through `@name`:
+
+----
+google-bigquery-sql://project-17248459:delete * from ${datasetId}.${tableId} 
where id=@myId
+----
+
+IMPORTANT: `$\{name}` is not parameter binding. Use it only for identifiers 
such as dataset
+and table names, and only with values that your route controls. A value 
containing SQL
+syntax changes the structure of the executed query instead of being treated as 
data, so a
+`$\{name}` placeholder must never be populated from message content supplied 
by an untrusted
+sender. Use `@name` for values.
+
 == Query Types
 
 The component supports both SELECT and DML (INSERT, UPDATE, DELETE) queries:
diff --git 
a/components/camel-google/camel-google-bigquery/src/main/java/org/apache/camel/component/google/bigquery/sql/SqlHelper.java
 
b/components/camel-google/camel-google-bigquery/src/main/java/org/apache/camel/component/google/bigquery/sql/SqlHelper.java
index 6f65f30ed833..ced79d07936d 100644
--- 
a/components/camel-google/camel-google-bigquery/src/main/java/org/apache/camel/component/google/bigquery/sql/SqlHelper.java
+++ 
b/components/camel-google/camel-google-bigquery/src/main/java/org/apache/camel/component/google/bigquery/sql/SqlHelper.java
@@ -29,9 +29,20 @@ import org.apache.camel.Message;
 import org.apache.camel.NoTypeConversionAvailableException;
 import org.apache.camel.RuntimeExchangeException;
 import org.apache.camel.support.ResourceHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public final class SqlHelper {
 
+    private static final Logger LOG = LoggerFactory.getLogger(SqlHelper.class);
+
+    /**
+     * Shape accepted for a placeholder substitution: a dot-separated sequence 
of project, dataset and table parts, each
+     * starting with a letter or an underscore.
+     */
+    private static final Pattern IDENTIFIER_PATTERN
+            = 
Pattern.compile("[A-Za-z_][A-Za-z0-9_-]*(\\.[A-Za-z_][A-Za-z0-9_-]*)*");
+
     private static Pattern pattern = Pattern.compile("\\$\\{(\\w+)}");
     private static Pattern parameterPattern = Pattern.compile("@(\\w+)");
 
@@ -58,6 +69,10 @@ public final class SqlHelper {
     /**
      * Replaces pattern in query in form of "${param}" with values from 
message header Raises an error if param value
      * not found in headers
+     * <p>
+     * The value is spliced into the query text verbatim, so this form is 
meant for dataset and table identifiers. A
+     * substitution that does not have the shape of an identifier is reported 
at WARN level; use a query parameter in
+     * the form {@code @name} to pass values.
      *
      * @param  exchange
      * @return          Translated query text
@@ -78,6 +93,14 @@ public final class SqlHelper {
                 }
             }
 
+            if (!isValidIdentifier(value)) {
+                LOG.warn("Placeholder '{}' was substituted with a value that 
is not a valid BigQuery identifier."
+                         + " Placeholders in the form ${name} are spliced into 
the query text verbatim and are"
+                         + " intended for dataset and table names; use a query 
parameter in the form @name to pass"
+                         + " values.",
+                        paramKey);
+            }
+
             String replacement = Matcher.quoteReplacement(value);
             matcher.appendReplacement(stringBuffer, replacement);
         }
@@ -85,6 +108,20 @@ public final class SqlHelper {
         return stringBuffer.toString();
     }
 
+    /**
+     * Whether the given value has the shape of a BigQuery identifier: a 
dot-separated sequence of project, dataset and
+     * table parts, each starting with a letter or an underscore.
+     * <p>
+     * Values that do not match are spliced into the query text verbatim and 
can therefore alter its structure, so
+     * {@link #translateQuery(String, Exchange)} reports them.
+     *
+     * @param  value the substituted value
+     * @return       true if the value has the shape of a BigQuery identifier
+     */
+    static boolean isValidIdentifier(String value) {
+        return IDENTIFIER_PATTERN.matcher(value).matches();
+    }
+
     /**
      * Extracts list of parameters in form "@name" from query text
      *
diff --git 
a/components/camel-google/camel-google-bigquery/src/test/java/org/apache/camel/component/google/bigquery/sql/SqlHelperIdentifierTest.java
 
b/components/camel-google/camel-google-bigquery/src/test/java/org/apache/camel/component/google/bigquery/sql/SqlHelperIdentifierTest.java
new file mode 100644
index 000000000000..1f414c158a72
--- /dev/null
+++ 
b/components/camel-google/camel-google-bigquery/src/test/java/org/apache/camel/component/google/bigquery/sql/SqlHelperIdentifierTest.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.google.bigquery.sql;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SqlHelperIdentifierTest {
+
+    @Test
+    public void testIdentifierShapedValuesAreAccepted() {
+        assertThat(SqlHelper.isValidIdentifier("report_data")).isTrue();
+        assertThat(SqlHelper.isValidIdentifier("_private")).isTrue();
+        assertThat(SqlHelper.isValidIdentifier("test.table")).isTrue();
+        
assertThat(SqlHelper.isValidIdentifier("project-17248459.dataset.table")).isTrue();
+    }
+
+    @Test
+    public void testValuesCarryingSqlSyntaxAreNotIdentifiers() {
+        assertThat(SqlHelper.isValidIdentifier("")).isFalse();
+        assertThat(SqlHelper.isValidIdentifier("1dataset")).isFalse();
+        assertThat(SqlHelper.isValidIdentifier("dataset table")).isFalse();
+        assertThat(SqlHelper.isValidIdentifier("table; DROP TABLE 
other")).isFalse();
+        assertThat(SqlHelper.isValidIdentifier("nope' UNION ALL SELECT 1 
--")).isFalse();
+    }
+}
diff --git 
a/components/camel-google/camel-google-bigquery/src/test/java/org/apache/camel/component/google/bigquery/unit/sql/SqlHelperTest.java
 
b/components/camel-google/camel-google-bigquery/src/test/java/org/apache/camel/component/google/bigquery/unit/sql/SqlHelperTest.java
index 3c46d4d02850..530f14dbfebd 100644
--- 
a/components/camel-google/camel-google-bigquery/src/test/java/org/apache/camel/component/google/bigquery/unit/sql/SqlHelperTest.java
+++ 
b/components/camel-google/camel-google-bigquery/src/test/java/org/apache/camel/component/google/bigquery/unit/sql/SqlHelperTest.java
@@ -27,6 +27,7 @@ import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -109,6 +110,16 @@ public class SqlHelperTest {
                 () -> SqlHelper.translateQuery(query, exchange));
     }
 
+    @Test
+    public void testTranslateQueryKeepsSubstitutingNonIdentifierValues() {
+        when(exchange.getMessage()).thenReturn(message);
+        when(message.getHeader(eq("custId"), 
eq(String.class))).thenReturn("O'Brien");
+
+        String answer = SqlHelper.translateQuery("SELECT id FROM orders WHERE 
customer_id = '${custId}'", exchange);
+
+        assertThat(answer).isEqualTo("SELECT id FROM orders WHERE customer_id 
= 'O'Brien'");
+    }
+
     @Test
     public void testExtractParameterNames() {
         Set<String> answer = SqlHelper.extractParameterNames(query);

Reply via email to