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"));
+    }
+}

Reply via email to