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 d792bf7f2f77 CAMEL-24869: camel-jbang - validator hint for sql queries
with :name parameters (camel-sql uses :#name)
d792bf7f2f77 is described below
commit d792bf7f2f7754348f36dc880e75a6bbb5b7d4f3
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 21 11:48:14 2026 +0200
CAMEL-24869: camel-jbang - validator hint for sql queries with :name
parameters (camel-sql uses :#name)
Fixes https://issues.apache.org/jira/browse/CAMEL-24869
A `:name` in a sql endpoint query that is not the camel-sql `:#name` (a
header or a key of a Map body) or `:#${simple}` form goes to the JDBC driver as
written and fails at runtime with a syntax error, after the file consumer
retried it a hundred times. The validator (`camel validate`,
`camel_validate_source`, `camel_write_file`) now names the parameter and says
how to write it:
```
Line 8: sql: :customer is not a camel-sql named parameter (the JDBC driver
gets it as written and fails with a syntax error): write :#customer for a
header or a key of a Map body, or :#${body[customer]} with a Simple expression
(also :country)
```
It covers the query under `parameters:` and the `sql:SELECT ...` path (the
uri pattern stops at the first space, so the statement is taken from the line).
`::type` casts, `:?name` stored procedure parameters, `:#...` and times such as
`'10:30'` are left alone; `sql-stored` and `jdbc` are not checked (their
statements have other forms).
Seen in the camel-jbang-mcp server stepwise benchmark on the sql example:
five of six first statements of the local model used `:customer` or
`:body[customer]`. Tests: `SourceValidatorSqlParametersTest`; every
documentation example still passes `CatalogDocExamplesTest`.
---
.../dsl/jbang/core/commands/ai/EndpointChecks.java | 65 ++++++++++++++++
.../ai/SourceValidatorSqlParametersTest.java | 89 ++++++++++++++++++++++
2 files changed, 154 insertions(+)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
index a16ae541175f..1a24f34daa9f 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
@@ -237,6 +237,7 @@ final class EndpointChecks {
checkDynamicDirectory(errors, fullUri, i, eipName);
checkSimplePlaceholders(errors, fullUri, i, optionLineMap,
eipName);
checkRequiredPathOptions(errors, fullUri, catalog, i, eipName);
+ checkSqlNamedParameters(errors, fullUri, line, i,
optionLineMap);
} catch (Exception e) {
// ignore validation errors
}
@@ -391,6 +392,70 @@ final class EndpointChecks {
+ "?fileName=${...}), or use toD: with the whole uri, which
evaluates it per message");
}
+ /**
+ * A :name in a sql query that is not a camel-sql named parameter:
camel-sql has :#name (a header, or a key of a Map
+ * body) and :#${simple}; a bare :name (the Spring or JPA form) goes to
the JDBC driver as written and fails at
+ * runtime with a syntax error, after the consumer retried it
(CAMEL-24869). ::type casts, :?name stored procedure
+ * parameters and times such as 10:30 are left alone.
+ */
+ private static final Pattern BARE_NAMED_PARAMETER =
Pattern.compile("(?<![\\w:#?$'\"]):([A-Za-z_][\\w.\\[\\]]*)");
+
+ static void checkSqlNamedParameters(
+ List<String> errors, String fullUri, String rawLine, int
uriLineIdx, Map<String, Integer> optionLineMap) {
+ int colon = fullUri.indexOf(':');
+ int q = fullUri.indexOf('?');
+ String scheme = colon < 0 ? (q < 0 ? fullUri : fullUri.substring(0,
q)) : fullUri.substring(0, colon);
+ if (q >= 0 && colon > q) {
+ scheme = fullUri.substring(0, q);
+ }
+ if (!"sql".equals(scheme)) {
+ return;
+ }
+ // the query is the path (sql:SELECT ...) or the query option (uri:
sql with parameters: query: ...)
+ String query = null;
+ int line = uriLineIdx;
+ if (colon >= 0 && (q < 0 || colon < q)) {
+ // the uri pattern stops at the first space, so take the statement
from the line itself
+ int at = rawLine.indexOf("sql:");
+ String path = at >= 0 ? rawLine.substring(at + 4).trim() :
fullUri.substring(colon + 1);
+ if (path.endsWith("\"") || path.endsWith("'")) {
+ path = path.substring(0, path.length() - 1);
+ }
+ Matcher options = Pattern.compile("\\?\\w+=").matcher(path);
+ query = options.find() ? path.substring(0, options.start()) : path;
+ }
+ if ((query == null || query.isBlank()) && q >= 0) {
+ for (String option : fullUri.substring(q + 1).split("&")) {
+ if (option.startsWith("query=")) {
+ query = option.substring("query=".length());
+ line = optionLineMap.getOrDefault("query", uriLineIdx);
+ }
+ }
+ }
+ if (query == null || query.isBlank()) {
+ return;
+ }
+ Matcher m = BARE_NAMED_PARAMETER.matcher(query);
+ List<String> bare = new ArrayList<>();
+ while (m.find()) {
+ String name = m.group(1);
+ if (!bare.contains(name)) {
+ bare.add(name);
+ }
+ }
+ if (bare.isEmpty()) {
+ return;
+ }
+ String first = bare.get(0);
+ // :customer -> :#customer (a header or a Map body key);
:body[customer] -> :#${body[customer]} (a Simple expression)
+ String fix = first.matches("\\w+")
+ ? ":#" + first + " for a header or a key of a Map body, or
:#${body[" + first + "]} with a Simple expression"
+ : ":#${" + first + "} (a Simple expression) or :#name for a
header or a key of a Map body";
+ errors.add(linePrefix(line) + "sql: :" + first + " is not a camel-sql
named parameter (the JDBC driver gets it as"
+ + " written and fails with a syntax error): write " + fix
+ + (bare.size() > 1 ? " (also :" + String.join(", :",
bare.subList(1, bare.size())) + ")" : ""));
+ }
+
/** The EIPs whose uri is a Simple expression evaluated per message:
${...} is right there. */
private static final Set<String> DYNAMIC_URI_EIPS = Set.of("toD", "to-d",
"wireTap", "wire-tap", "enrich", "pollEnrich",
"poll-enrich", "recipientList", "recipient-list", "routingSlip",
"routing-slip", "dynamicRouter", "dynamic-router");
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSqlParametersTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSqlParametersTest.java
new file mode 100644
index 000000000000..6aac64e0b9e4
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSqlParametersTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.dsl.jbang.core.commands.ai;
+
+import java.util.List;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-24869: a :name in a sql query that is not the camel-sql :#name form
goes to the JDBC driver as written and
+ * fails at runtime; the validator says how to write it.
+ */
+class SourceValidatorSqlParametersTest {
+
+ private static final CamelCatalog catalog = new DefaultCamelCatalog();
+
+ @Test
+ void aBareNamedParameterUnderParametersSaysToWriteTheCamelForm() {
+ String yaml = """
+ - route:
+ from:
+ uri: file:orders
+ steps:
+ - to:
+ uri: sql
+ parameters:
+ query: "INSERT INTO customers (id, country)
VALUES (:customer, :country)"
+ """;
+ List<String> errors = SourceValidator.validateYamlEndpoints(yaml,
catalog);
+ assertThat(errors).anyMatch(e -> e.startsWith("Line 8: sql: :customer
is not a camel-sql named parameter")
+ && e.contains("write :#customer for a header or a key of a Map
body, or :#${body[customer]}")
+ && e.endsWith("(also :country)"));
+ }
+
+ @Test
+ void aSimpleLookingNameInTheUriPathGetsTheSimpleForm() {
+ String yaml = """
+ - route:
+ from:
+ uri: file:orders
+ steps:
+ - to:
+ uri: "sql:INSERT INTO customers (id) VALUES
(:body[customer])"
+ """;
+ List<String> errors = SourceValidator.validateYamlEndpoints(yaml,
catalog);
+ assertThat(errors).anyMatch(e -> e.startsWith("Line 6: sql:
:body[customer] is not a camel-sql named parameter")
+ && e.contains("write :#${body[customer]} (a Simple
expression)"));
+ }
+
+ @Test
+ void theCamelFormsCastsTimesAndOtherComponentsAreFine() {
+ String yaml
+ = """
+ - route:
+ from:
+ uri: file:orders
+ steps:
+ - to:
+ uri: sql
+ parameters:
+ query: "MERGE INTO customers USING
(VALUES (:#${body[customer]}, :#country)) AS s(id, country) ON customers.id =
s.id WHEN MATCHED THEN UPDATE SET seen = '10:30', n = id::int"
+ - to:
+ uri: sql-stored
+ parameters:
+ template: "ADDNUMBERS(INTEGER
${header.a}, OUT INTEGER :#result)"
+ - to:
+ uri: "log:done?showHeaders=true"
+ """;
+ assertThat(SourceValidator.validateYamlEndpoints(yaml,
catalog)).noneMatch(e -> e.contains("named parameter"));
+ }
+}