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