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 f8c76e97f9fe CAMEL-26067: camel-jdbc - plain ? placeholders take their 
values only from the CamelJdbcParameters header (#29282)
f8c76e97f9fe is described below

commit f8c76e97f9fea4fa84a7394500bfe64c9fed8af8
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 09:50:46 2026 +0100

    CAMEL-26067: camel-jdbc - plain ? placeholders take their values only from 
the CamelJdbcParameters header (#29282)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/components/jdbc.json  |   6 +-
 .../camel/catalog/components/spring-jdbc.json      |   6 +-
 .../apache/camel/catalog/docs/jdbc-component.adoc  |  22 ++-
 .../org/apache/camel/component/jdbc/jdbc.json      |   6 +-
 .../camel-jdbc/src/main/docs/jdbc-component.adoc   |  22 ++-
 .../jdbc/DefaultJdbcPrepareStatementStrategy.java  |  62 ++++++--
 .../apache/camel/component/jdbc/JdbcConstants.java |   8 +-
 .../apache/camel/component/jdbc/JdbcEndpoint.java  |   9 +-
 .../component/jdbc/JdbcParameterizedQueryTest.java | 176 ++++++++++++++++++++-
 .../camel/component/spring/jdbc/spring-jdbc.json   |   6 +-
 .../component/spring/jdbc/SpringJdbcEndpoint.java  |   5 +-
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  17 ++
 .../endpoint/dsl/JdbcEndpointBuilderFactory.java   |  16 +-
 .../dsl/SpringJdbcEndpointBuilderFactory.java      |  16 +-
 14 files changed, 326 insertions(+), 51 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jdbc.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jdbc.json
index e05623176303..e187d94488f1 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jdbc.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jdbc.json
@@ -33,14 +33,14 @@
     "CamelJdbcUpdateCount": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": "int", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "If the query is an UPDATE, query the update count is 
returned in this OUT header.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_UPDATE_COUNT" },
     "CamelJdbcRowCount": { "index": 1, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": "int", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "If the query is a SELECT and the outputType is 
SelectList, the row count is returned in this OUT header.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_ROW_COUNT" },
     "CamelJdbcColumnNames": { "index": 2, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Set<String>", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The column names from the ResultSet as a 
java.util.Set type.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_COLUMN_NAMES" },
-    "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": "Map", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "A java.util.Map which has the headers to be used if 
useHeadersAsParameters has been enabled.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_PARAMETERS" },
+    "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Object", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The values of the query parameters when 
useHeadersAsParameters has been enabled: a java.util.Map with the values of the 
named parameters, or a java.util.List with the values of the positional 
placeholders, in order (a single value for [...]
     "CamelRetrieveGeneratedKeys": { "index": 4, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "defaultValue": "false", "description": "Set its value 
to true to retrieve generated keys", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_RETRIEVE_GENERATED_KEYS" },
     "CamelGeneratedColumns": { "index": 5, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"String[] or int[]", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Set it to specify the expected 
generated columns", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_COLUMNS" },
     "CamelGeneratedKeysRowCount": { "index": 6, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of rows in the header that 
contains generated keys.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_ROW_COUNT" },
     "CamelGeneratedKeysRows": { "index": 7, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"List<Map<String, Object>>", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Rows that contains the 
generated keys.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_DATA" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], 
"description": "The SQL statement as text, or bytes, a stream or a file of it; 
a Map or a List is not a statement: set the SQL as the body. With 
useHeadersAsParameters, the values of its placeholders come from the 
CamelJdbcParameters header, or else from the headers" },
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], 
"description": "The SQL statement as text, or bytes, a stream or a file of it; 
a Map or a List is not a statement: set the SQL as the body. With 
useHeadersAsParameters, the values of its named parameters (:?name) come from 
the CamelJdbcParameters header, or else from the headers, and the values of 
plain ? placeholders, in order, only from the CamelJdbcParameters header" },
     "producerOut": { "carriers": [ "list", "unchanged" ], "byOption": [ { 
"option": "outputType", "value": "SelectOne", "carriers": [ "map", "text", 
"value", "bytes", "pojo", "null", "unchanged" ], "javaTypeOption": 
"outputClass" }, { "option": "outputType", "value": "StreamList", "carriers": [ 
"iterator", "unchanged" ] } ], "description": "The rows of a select: a List of 
Maps of the column names and their values (beans of outputClass when set). 
SelectOne gives the Map of the one row, th [...]
   },
   "properties": {
@@ -53,7 +53,7 @@
     "resetAutoCommit": { "index": 6, "kind": "parameter", "displayName": 
"Reset Auto Commit", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": true, "description": "Camel will set 
the autoCommit on the JDBC connection to be false, commit the change after 
executed the statement and reset the autoCommit flag of the connection at the 
end, if the resetAutoCommit is true. If [...]
     "transacted": { "index": 7, "kind": "parameter", "displayName": 
"Transacted", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": true, "deprecationNote": "Not 
used: the producer does not read this option", "autowired": false, "secret": 
false, "defaultValue": false, "description": "Whether transactions are in use. 
Deprecation note: Not used: the producer does not read this option" },
     "useGetBytesForBlob": { "index": 8, "kind": "parameter", "displayName": 
"Use Get Bytes For Blob", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "To read BLOB 
columns as bytes (a byte) instead of a java.sql.Blob, which cannot be read 
after the producer has closed the connection. This may be needed for certain 
databases such as Oracle where yo [...]
-    "useHeadersAsParameters": { "index": 9, "kind": "parameter", 
"displayName": "Use Headers As Parameters", "group": "producer", "label": "", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Set this option to true to use the prepareStatementStrategy 
with named parameters. This allows to define queries with named placeholders, 
and use headers with the dynamic values for the query [...]
+    "useHeadersAsParameters": { "index": 9, "kind": "parameter", 
"displayName": "Use Headers As Parameters", "group": "producer", "label": "", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Set this option to true to use the prepareStatementStrategy 
with named parameters. This allows to define queries with named placeholders, 
and use headers with the dynamic values for the query [...]
     "useJDBC4ColumnNameAndLabelSemantics": { "index": 10, "kind": "parameter", 
"displayName": "Use JDBC4 Column Name And Label Semantics", "group": 
"producer", "label": "", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": true, "description": "Sets whether to use JDBC 4 or JDBC 3.0 or 
older semantic when retrieving column name. JDBC 4.0 uses columnLabel to get 
the column name where as JDBC 3.0 uses both [...]
     "lazyStartProducer": { "index": 11, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
     "beanRowMapper": { "index": 12, "kind": "parameter", "displayName": "Bean 
Row Mapper", "group": "advanced", "label": "advanced", "required": false, 
"type": "object", "javaType": "org.apache.camel.component.jdbc.BeanRowMapper", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
use a custom org.apache.camel.component.jdbc.BeanRowMapper when using 
outputClass. The default implementation will lower case the row names and skip 
underscores, and dashes. For exampl [...]
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-jdbc.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-jdbc.json
index 905518eb3d89..22babf6bf36d 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-jdbc.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-jdbc.json
@@ -33,14 +33,14 @@
     "CamelJdbcUpdateCount": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": "int", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "If the query is an UPDATE, query the update count is 
returned in this OUT header.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_UPDATE_COUNT" },
     "CamelJdbcRowCount": { "index": 1, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": "int", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "If the query is a SELECT and the outputType is 
SelectList, the row count is returned in this OUT header.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_ROW_COUNT" },
     "CamelJdbcColumnNames": { "index": 2, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Set<String>", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The column names from the ResultSet as a 
java.util.Set type.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_COLUMN_NAMES" },
-    "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": "Map", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "A java.util.Map which has the headers to be used if 
useHeadersAsParameters has been enabled.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_PARAMETERS" },
+    "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Object", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The values of the query parameters when 
useHeadersAsParameters has been enabled: a java.util.Map with the values of the 
named parameters, or a java.util.List with the values of the positional 
placeholders, in order (a single value for [...]
     "CamelRetrieveGeneratedKeys": { "index": 4, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "defaultValue": "false", "description": "Set its value 
to true to retrieve generated keys", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_RETRIEVE_GENERATED_KEYS" },
     "CamelGeneratedColumns": { "index": 5, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"String[] or int[]", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Set it to specify the expected 
generated columns", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_COLUMNS" },
     "CamelGeneratedKeysRowCount": { "index": 6, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of rows in the header that 
contains generated keys.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_ROW_COUNT" },
     "CamelGeneratedKeysRows": { "index": 7, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"List<Map<String, Object>>", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Rows that contains the 
generated keys.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_DATA" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], 
"description": "The SQL statement as text, or bytes, a stream or a file of it; 
a Map or a List is not a statement: set the SQL as the body. With 
useHeadersAsParameters, the values of its placeholders come from the 
CamelJdbcParameters header, or else from the headers" },
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], 
"description": "The SQL statement as text, or bytes, a stream or a file of it; 
a Map or a List is not a statement: set the SQL as the body. With 
useHeadersAsParameters, the values of its named parameters (:?name) come from 
the CamelJdbcParameters header, or else from the headers, and the values of 
plain ? placeholders, in order, only from the CamelJdbcParameters header" },
     "producerOut": { "carriers": [ "list", "unchanged" ], "byOption": [ { 
"option": "outputType", "value": "SelectOne", "carriers": [ "map", "text", 
"value", "bytes", "pojo", "null", "unchanged" ], "javaTypeOption": 
"outputClass" }, { "option": "outputType", "value": "StreamList", "carriers": [ 
"iterator", "unchanged" ] } ], "description": "The rows of a select: a List of 
Maps of the column names and their values (beans of outputClass when set). 
SelectOne gives the Map of the one row, th [...]
   },
   "properties": {
@@ -53,7 +53,7 @@
     "resetAutoCommit": { "index": 6, "kind": "parameter", "displayName": 
"Reset Auto Commit", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": true, "description": "Camel will set 
the autoCommit on the JDBC connection to be false, commit the change after 
executed the statement and reset the autoCommit flag of the connection at the 
end, if the resetAutoCommit is true. If [...]
     "transacted": { "index": 7, "kind": "parameter", "displayName": 
"Transacted", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": true, "deprecationNote": "Not 
used: the producer does not read this option", "autowired": false, "secret": 
false, "defaultValue": false, "description": "Deprecation note: Not used: the 
producer does not read this option" },
     "useGetBytesForBlob": { "index": 8, "kind": "parameter", "displayName": 
"Use Get Bytes For Blob", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "To read BLOB 
columns as bytes (a byte) instead of a java.sql.Blob, which cannot be read 
after the producer has closed the connection. This may be needed for certain 
databases such as Oracle where yo [...]
-    "useHeadersAsParameters": { "index": 9, "kind": "parameter", 
"displayName": "Use Headers As Parameters", "group": "producer", "label": "", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Set this option to true to use the prepareStatementStrategy 
with named parameters. This allows to define queries with named placeholders, 
and use headers with the dynamic values for the query [...]
+    "useHeadersAsParameters": { "index": 9, "kind": "parameter", 
"displayName": "Use Headers As Parameters", "group": "producer", "label": "", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Set this option to true to use the prepareStatementStrategy 
with named parameters. This allows to define queries with named placeholders, 
and use headers with the dynamic values for the query [...]
     "useJDBC4ColumnNameAndLabelSemantics": { "index": 10, "kind": "parameter", 
"displayName": "Use JDBC4 Column Name And Label Semantics", "group": 
"producer", "label": "", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": true, "description": "Sets whether to use JDBC 4 or JDBC 3.0 or 
older semantic when retrieving column name. JDBC 4.0 uses columnLabel to get 
the column name where as JDBC 3.0 uses both [...]
     "lazyStartProducer": { "index": 11, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
     "beanRowMapper": { "index": 12, "kind": "parameter", "displayName": "Bean 
Row Mapper", "group": "advanced", "label": "advanced", "required": false, 
"type": "object", "javaType": "org.apache.camel.component.jdbc.BeanRowMapper", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
use a custom org.apache.camel.component.jdbc.BeanRowMapper when using 
outputClass. The default implementation will lower case the row names and skip 
underscores, and dashes. For exampl [...]
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jdbc-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jdbc-component.adoc
index c055e8f3c8b2..96f9b8bc23d8 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jdbc-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jdbc-component.adoc
@@ -157,7 +157,27 @@ YAML::
 ====
 
 You can also store the header values in a `java.util.Map` and store the
-map on the headers with the key `CamelJdbcParameters`.
+map on the headers with the key `CamelJdbcParameters`. When 
`CamelJdbcParameters` is not a `Map` (such as the `List`
+of an earlier query with plain `?` placeholders in the same route), the named 
parameters take their values from the
+message headers.
+
+=== Using plain ? placeholders
+
+A query with plain `?` placeholders (and no named parameters) takes the values 
of the placeholders, in order, only from
+the `CamelJdbcParameters` header, as a `java.util.List` (a `java.util.Map` 
works too, with its values taken in the order
+of its entries, so use a `LinkedHashMap`). An object array works like a 
`List`, and any other value is the value of a
+query with one `?` placeholder: a `String` is not split by comma, and a 
`byte[]` is one value. The other message
+headers are not used. When the header is not set, or has more or fewer values 
than the query has `?` placeholders,
+the exchange fails.
+
+._Java-only: inline Processor that sets the List of the values_
+[source,java]
+----
+from("direct:projects")
+    .process(exchange -> 
exchange.getIn().setHeader(JdbcConstants.JDBC_PARAMETERS, List.of("ASF", 123)))
+    .setBody(constant("select * from projects where license = ? and id > ? 
order by id"))
+    .to("jdbc:myDataSource?useHeadersAsParameters=true");
+----
 
 == Examples
 
diff --git 
a/components/camel-jdbc/src/generated/resources/META-INF/org/apache/camel/component/jdbc/jdbc.json
 
b/components/camel-jdbc/src/generated/resources/META-INF/org/apache/camel/component/jdbc/jdbc.json
index e05623176303..e187d94488f1 100644
--- 
a/components/camel-jdbc/src/generated/resources/META-INF/org/apache/camel/component/jdbc/jdbc.json
+++ 
b/components/camel-jdbc/src/generated/resources/META-INF/org/apache/camel/component/jdbc/jdbc.json
@@ -33,14 +33,14 @@
     "CamelJdbcUpdateCount": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": "int", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "If the query is an UPDATE, query the update count is 
returned in this OUT header.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_UPDATE_COUNT" },
     "CamelJdbcRowCount": { "index": 1, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": "int", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "If the query is a SELECT and the outputType is 
SelectList, the row count is returned in this OUT header.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_ROW_COUNT" },
     "CamelJdbcColumnNames": { "index": 2, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Set<String>", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The column names from the ResultSet as a 
java.util.Set type.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_COLUMN_NAMES" },
-    "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": "Map", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "A java.util.Map which has the headers to be used if 
useHeadersAsParameters has been enabled.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_PARAMETERS" },
+    "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Object", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The values of the query parameters when 
useHeadersAsParameters has been enabled: a java.util.Map with the values of the 
named parameters, or a java.util.List with the values of the positional 
placeholders, in order (a single value for [...]
     "CamelRetrieveGeneratedKeys": { "index": 4, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "defaultValue": "false", "description": "Set its value 
to true to retrieve generated keys", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_RETRIEVE_GENERATED_KEYS" },
     "CamelGeneratedColumns": { "index": 5, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"String[] or int[]", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Set it to specify the expected 
generated columns", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_COLUMNS" },
     "CamelGeneratedKeysRowCount": { "index": 6, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of rows in the header that 
contains generated keys.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_ROW_COUNT" },
     "CamelGeneratedKeysRows": { "index": 7, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"List<Map<String, Object>>", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Rows that contains the 
generated keys.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_DATA" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], 
"description": "The SQL statement as text, or bytes, a stream or a file of it; 
a Map or a List is not a statement: set the SQL as the body. With 
useHeadersAsParameters, the values of its placeholders come from the 
CamelJdbcParameters header, or else from the headers" },
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], 
"description": "The SQL statement as text, or bytes, a stream or a file of it; 
a Map or a List is not a statement: set the SQL as the body. With 
useHeadersAsParameters, the values of its named parameters (:?name) come from 
the CamelJdbcParameters header, or else from the headers, and the values of 
plain ? placeholders, in order, only from the CamelJdbcParameters header" },
     "producerOut": { "carriers": [ "list", "unchanged" ], "byOption": [ { 
"option": "outputType", "value": "SelectOne", "carriers": [ "map", "text", 
"value", "bytes", "pojo", "null", "unchanged" ], "javaTypeOption": 
"outputClass" }, { "option": "outputType", "value": "StreamList", "carriers": [ 
"iterator", "unchanged" ] } ], "description": "The rows of a select: a List of 
Maps of the column names and their values (beans of outputClass when set). 
SelectOne gives the Map of the one row, th [...]
   },
   "properties": {
@@ -53,7 +53,7 @@
     "resetAutoCommit": { "index": 6, "kind": "parameter", "displayName": 
"Reset Auto Commit", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": true, "description": "Camel will set 
the autoCommit on the JDBC connection to be false, commit the change after 
executed the statement and reset the autoCommit flag of the connection at the 
end, if the resetAutoCommit is true. If [...]
     "transacted": { "index": 7, "kind": "parameter", "displayName": 
"Transacted", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": true, "deprecationNote": "Not 
used: the producer does not read this option", "autowired": false, "secret": 
false, "defaultValue": false, "description": "Whether transactions are in use. 
Deprecation note: Not used: the producer does not read this option" },
     "useGetBytesForBlob": { "index": 8, "kind": "parameter", "displayName": 
"Use Get Bytes For Blob", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "To read BLOB 
columns as bytes (a byte) instead of a java.sql.Blob, which cannot be read 
after the producer has closed the connection. This may be needed for certain 
databases such as Oracle where yo [...]
-    "useHeadersAsParameters": { "index": 9, "kind": "parameter", 
"displayName": "Use Headers As Parameters", "group": "producer", "label": "", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Set this option to true to use the prepareStatementStrategy 
with named parameters. This allows to define queries with named placeholders, 
and use headers with the dynamic values for the query [...]
+    "useHeadersAsParameters": { "index": 9, "kind": "parameter", 
"displayName": "Use Headers As Parameters", "group": "producer", "label": "", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Set this option to true to use the prepareStatementStrategy 
with named parameters. This allows to define queries with named placeholders, 
and use headers with the dynamic values for the query [...]
     "useJDBC4ColumnNameAndLabelSemantics": { "index": 10, "kind": "parameter", 
"displayName": "Use JDBC4 Column Name And Label Semantics", "group": 
"producer", "label": "", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": true, "description": "Sets whether to use JDBC 4 or JDBC 3.0 or 
older semantic when retrieving column name. JDBC 4.0 uses columnLabel to get 
the column name where as JDBC 3.0 uses both [...]
     "lazyStartProducer": { "index": 11, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
     "beanRowMapper": { "index": 12, "kind": "parameter", "displayName": "Bean 
Row Mapper", "group": "advanced", "label": "advanced", "required": false, 
"type": "object", "javaType": "org.apache.camel.component.jdbc.BeanRowMapper", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
use a custom org.apache.camel.component.jdbc.BeanRowMapper when using 
outputClass. The default implementation will lower case the row names and skip 
underscores, and dashes. For exampl [...]
diff --git a/components/camel-jdbc/src/main/docs/jdbc-component.adoc 
b/components/camel-jdbc/src/main/docs/jdbc-component.adoc
index c055e8f3c8b2..96f9b8bc23d8 100644
--- a/components/camel-jdbc/src/main/docs/jdbc-component.adoc
+++ b/components/camel-jdbc/src/main/docs/jdbc-component.adoc
@@ -157,7 +157,27 @@ YAML::
 ====
 
 You can also store the header values in a `java.util.Map` and store the
-map on the headers with the key `CamelJdbcParameters`.
+map on the headers with the key `CamelJdbcParameters`. When 
`CamelJdbcParameters` is not a `Map` (such as the `List`
+of an earlier query with plain `?` placeholders in the same route), the named 
parameters take their values from the
+message headers.
+
+=== Using plain ? placeholders
+
+A query with plain `?` placeholders (and no named parameters) takes the values 
of the placeholders, in order, only from
+the `CamelJdbcParameters` header, as a `java.util.List` (a `java.util.Map` 
works too, with its values taken in the order
+of its entries, so use a `LinkedHashMap`). An object array works like a 
`List`, and any other value is the value of a
+query with one `?` placeholder: a `String` is not split by comma, and a 
`byte[]` is one value. The other message
+headers are not used. When the header is not set, or has more or fewer values 
than the query has `?` placeholders,
+the exchange fails.
+
+._Java-only: inline Processor that sets the List of the values_
+[source,java]
+----
+from("direct:projects")
+    .process(exchange -> 
exchange.getIn().setHeader(JdbcConstants.JDBC_PARAMETERS, List.of("ASF", 123)))
+    .setBody(constant("select * from projects where license = ? and id > ? 
order by id"))
+    .to("jdbc:myDataSource?useHeadersAsParameters=true");
+----
 
 == Examples
 
diff --git 
a/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/DefaultJdbcPrepareStatementStrategy.java
 
b/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/DefaultJdbcPrepareStatementStrategy.java
index e494c44fbbb5..551d8f21911b 100644
--- 
a/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/DefaultJdbcPrepareStatementStrategy.java
+++ 
b/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/DefaultJdbcPrepareStatementStrategy.java
@@ -18,7 +18,10 @@ package org.apache.camel.component.jdbc;
 
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -55,19 +58,21 @@ public class DefaultJdbcPrepareStatementStrategy implements 
JdbcPrepareStatement
             final String query, final String preparedQuery, final int 
expectedParams,
             final Exchange exchange, final Object value)
             throws SQLException {
-        Map<?, ?> map = null;
-        if (exchange.getIn().hasHeaders()) {
-            if (exchange.getIn().getHeader(JdbcConstants.JDBC_PARAMETERS) != 
null) {
-                // header JDBC_PARAMETERS takes precedence over regular headers
-                map = 
exchange.getIn().getHeader(JdbcConstants.JDBC_PARAMETERS, Map.class);
-            } else {
-                map = exchange.getIn().getHeaders();
+        if (hasNamedParameters(query)) {
+            Map<?, ?> map = null;
+            if (exchange.getIn().hasHeaders()) {
+                if (exchange.getIn().getHeader(JdbcConstants.JDBC_PARAMETERS) 
instanceof Map<?, ?> parameters) {
+                    // header JDBC_PARAMETERS takes precedence over regular 
headers
+                    map = parameters;
+                } else {
+                    // a JDBC_PARAMETERS header that is not a Map (such as the 
List of the values of an earlier query
+                    // with plain ? placeholders) has no names, so the values 
come from the headers
+                    map = exchange.getIn().getHeaders();
+                }
             }
-        }
 
-        final Map<?, ?> headerMap = map;
+            final Map<?, ?> headerMap = map;
 
-        if (hasNamedParameters(query)) {
             // create an iterator that returns the value in the named order
             try {
 
@@ -118,10 +123,41 @@ public class DefaultJdbcPrepareStatementStrategy 
implements JdbcPrepareStatement
             }
 
         } else {
-            // just use a regular iterator
-            return 
exchange.getContext().getTypeConverter().convertTo(Iterator.class,
-                    headerMap != null ? headerMap.values() : null);
+            // plain ? placeholders are bound by position, so the values are 
only taken from the JDBC_PARAMETERS header
+            // (and not from all the message headers, as their order does not 
relate to the query)
+            return createPositionalIterator(query, expectedParams, exchange);
+        }
+    }
+
+    private Iterator<?> createPositionalIterator(String query, int 
expectedParams, Exchange exchange) {
+        Object parameters = 
exchange.getIn().getHeader(JdbcConstants.JDBC_PARAMETERS);
+        if (parameters == null) {
+            throw new RuntimeExchangeException(
+                    "Cannot set the ? placeholders in query [" + query + "]: 
set their values, in order, as a List in the "
+                                               + JdbcConstants.JDBC_PARAMETERS
+                                               + " header, or use named 
parameters (:?name)",
+                    exchange);
+        }
+        List<Object> values = new ArrayList<>();
+        if (parameters instanceof Map<?, ?> map) {
+            values.addAll(map.values());
+        } else if (parameters instanceof Iterable<?> iterable) {
+            iterable.forEach(values::add);
+        } else if (parameters instanceof Object[] array) {
+            values.addAll(Arrays.asList(array));
+        } else {
+            // any other value is the one value, such as a String (not split 
by comma) or a byte[] (not one value
+            // per byte)
+            values.add(parameters);
+        }
+        if (values.size() != expectedParams) {
+            throw new RuntimeExchangeException(
+                    "Query [" + query + "] has " + expectedParams + " ? 
placeholders but the "
+                                               + JdbcConstants.JDBC_PARAMETERS 
+ " header has " + values.size()
+                                               + " values",
+                    exchange);
         }
+        return values.iterator();
     }
 
     @Override
diff --git 
a/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcConstants.java
 
b/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcConstants.java
index 786c9f1c7606..9c5319bea2b2 100644
--- 
a/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcConstants.java
+++ 
b/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcConstants.java
@@ -35,9 +35,11 @@ public final class JdbcConstants {
                                                 "type.",
               javaType = "Set<String>")
     public static final String JDBC_COLUMN_NAMES = "CamelJdbcColumnNames";
-    @Metadata(label = "producer", description = "A `java.util.Map` which has 
the headers to be used if\n" +
-                                                "`useHeadersAsParameters` has 
been enabled.",
-              javaType = "Map")
+    @Metadata(label = "producer",
+              description = "The values of the query parameters when 
`useHeadersAsParameters` has been enabled: a\n" +
+                            "`java.util.Map` with the values of the named 
parameters, or a `java.util.List` with the\n" +
+                            "values of the positional placeholders, in order 
(a single value for one placeholder).",
+              javaType = "Object")
     public static final String JDBC_PARAMETERS = "CamelJdbcParameters";
 
     /**
diff --git 
a/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcEndpoint.java
 
b/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcEndpoint.java
index 26545f4053d2..d5a146c7986a 100644
--- 
a/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcEndpoint.java
+++ 
b/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcEndpoint.java
@@ -42,8 +42,9 @@ import org.apache.camel.support.DefaultEndpoint;
 @BodyTypes(producerIn = @BodyType(value = { Carrier.TEXT, Carrier.BYTES, 
Carrier.STREAM, Carrier.FILE },
                                   description = "The SQL statement as text, or 
bytes, a stream or a file of it; a Map or a List"
                                                 + " is not a statement: set 
the SQL as the body. With useHeadersAsParameters,"
-                                                + " the values of its 
placeholders come from the CamelJdbcParameters header, or"
-                                                + " else from the headers"),
+                                                + " the values of its named 
parameters (:?name) come from the"
+                                                + " CamelJdbcParameters 
header, or else from the headers, and the values of"
+                                                + " plain ? placeholders, in 
order, only from the CamelJdbcParameters header"),
            producerOut = @BodyType(value = { Carrier.LIST, Carrier.UNCHANGED },
                                    byOption = {
                                            @BodyType.When(option = 
"outputType", value = "SelectOne",
@@ -243,7 +244,9 @@ public class JdbcEndpoint extends DefaultEndpoint {
 
     /**
      * Set this option to true to use the prepareStatementStrategy with named 
parameters. This allows to define queries
-     * with named placeholders, and use headers with the dynamic values for 
the query placeholders.
+     * with named placeholders, and use headers with the dynamic values for 
the query placeholders. A query with only
+     * positional placeholders (no named parameters) takes their values, in 
order, only from the CamelJdbcParameters
+     * header (a List).
      */
     public void setUseHeadersAsParameters(boolean useHeadersAsParameters) {
         this.useHeadersAsParameters = useHeadersAsParameters;
diff --git 
a/components/camel-jdbc/src/test/java/org/apache/camel/component/jdbc/JdbcParameterizedQueryTest.java
 
b/components/camel-jdbc/src/test/java/org/apache/camel/component/jdbc/JdbcParameterizedQueryTest.java
index e573f022392a..990776ea2745 100644
--- 
a/components/camel-jdbc/src/test/java/org/apache/camel/component/jdbc/JdbcParameterizedQueryTest.java
+++ 
b/components/camel-jdbc/src/test/java/org/apache/camel/component/jdbc/JdbcParameterizedQueryTest.java
@@ -17,15 +17,21 @@
 package org.apache.camel.component.jdbc;
 
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.RuntimeExchangeException;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.mock.MockEndpoint;
 import org.junit.jupiter.api.Test;
 
 import static org.apache.camel.test.junit6.TestSupport.assertIsInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 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 JdbcParameterizedQueryTest extends AbstractJdbcTestSupport {
 
@@ -34,13 +40,14 @@ public class JdbcParameterizedQueryTest extends 
AbstractJdbcTestSupport {
         MockEndpoint mock = getMockEndpoint("mock:result");
         mock.expectedMessageCount(1);
 
-        // The linkedHashMap values has different order in JDK7 and JDK8
-        // so I had to reduce the parameters size
-        Map<String, Object> jdbcParams = new HashMap<>();
-        jdbcParams.put("name", "jstrachan");
+        Map<String, Object> headers = new LinkedHashMap<>();
+        // other headers are not bound to the ? placeholders
+        headers.put("foo", "cust2");
+        headers.put("bar", "nsandhu");
+        headers.put(JdbcConstants.JDBC_PARAMETERS, List.of("cust1", 
"jstrachan"));
 
-        template.sendBodyAndHeaders("direct:start", "select * from customer 
where id = 'cust1' and name = ? order by ID",
-                jdbcParams);
+        template.sendBodyAndHeaders("direct:start", "select * from customer 
where id = ? and name = ? order by ID",
+                headers);
 
         MockEndpoint.assertIsSatisfied(context);
 
@@ -50,6 +57,156 @@ public class JdbcParameterizedQueryTest extends 
AbstractJdbcTestSupport {
         assertEquals("jstrachan", row.get("NAME"));
     }
 
+    @Test
+    public void testParameterizedQueryNoNamesMap() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+
+        // the values of a map are bound in the order of its entries
+        Map<String, Object> jdbcParams = new LinkedHashMap<>();
+        jdbcParams.put("id", "cust3");
+        jdbcParams.put("name", "willem");
+
+        Map<String, Object> headers = new LinkedHashMap<>();
+        headers.put("foo", "cust2");
+        headers.put(JdbcConstants.JDBC_PARAMETERS, jdbcParams);
+
+        template.sendBodyAndHeaders("direct:start", "select * from customer 
where id = ? and name = ? order by ID",
+                headers);
+
+        MockEndpoint.assertIsSatisfied(context);
+
+        List<?> received = assertIsInstanceOf(List.class, 
mock.getReceivedExchanges().get(0).getIn().getBody());
+        assertEquals(1, received.size());
+        Map<?, ?> row = assertIsInstanceOf(Map.class, received.get(0));
+        assertEquals("willem", row.get("NAME"));
+    }
+
+    @Test
+    public void testParameterizedQueryNoNamesNotFromHeaders() {
+        Map<String, Object> headers = new LinkedHashMap<>();
+        headers.put("id", "cust1");
+        headers.put("name", "jstrachan");
+
+        CamelExecutionException e = assertThrows(CamelExecutionException.class,
+                () -> template.sendBodyAndHeaders("direct:start",
+                        "select * from customer where id = ? and name = ? 
order by ID", headers));
+        RuntimeExchangeException cause = 
assertIsInstanceOf(RuntimeExchangeException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("as a List in the 
CamelJdbcParameters header"), cause.getMessage());
+    }
+
+    @Test
+    public void testParameterizedQueryNoNamesSingleValue() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+
+        // a single value is the value of the one ? placeholder (a String is 
not split by comma)
+        template.sendBodyAndHeader("direct:start", "select * from customer 
where id = ? or name = 'a,b' order by ID",
+                JdbcConstants.JDBC_PARAMETERS, "cust2");
+
+        MockEndpoint.assertIsSatisfied(context);
+
+        List<?> received = assertIsInstanceOf(List.class, 
mock.getReceivedExchanges().get(0).getIn().getBody());
+        assertEquals(1, received.size());
+        Map<?, ?> row = assertIsInstanceOf(Map.class, received.get(0));
+        assertEquals("nsandhu", row.get("NAME"));
+    }
+
+    @Test
+    public void testParameterizedQueryNoNamesSingleValueWithComma() {
+        // a String with a comma is one value, so it does not fill two ? 
placeholders
+        CamelExecutionException e = assertThrows(CamelExecutionException.class,
+                () -> template.sendBodyAndHeader("direct:start",
+                        "select * from customer where id = ? and name = ? 
order by ID", JdbcConstants.JDBC_PARAMETERS,
+                        "cust1,jstrachan"));
+        RuntimeExchangeException cause = 
assertIsInstanceOf(RuntimeExchangeException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("has 2 ? placeholders but the 
CamelJdbcParameters header has 1 values"),
+                cause.getMessage());
+    }
+
+    @Test
+    public void testParameterizedQueryNoNamesTooManyValues() {
+        // more values than placeholders is a mistake in the route, and not 
silently ignored
+        CamelExecutionException e = assertThrows(CamelExecutionException.class,
+                () -> template.sendBodyAndHeader("direct:start",
+                        "select * from customer where id = ? and name = ? 
order by ID", JdbcConstants.JDBC_PARAMETERS,
+                        List.of("cust1", "jstrachan", "unused")));
+        RuntimeExchangeException cause = 
assertIsInstanceOf(RuntimeExchangeException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("has 2 ? placeholders but the 
CamelJdbcParameters header has 3 values"),
+                cause.getMessage());
+    }
+
+    @Test
+    public void testParameterizedQueryNoNamesTooFewValues() {
+        CamelExecutionException e = assertThrows(CamelExecutionException.class,
+                () -> template.sendBodyAndHeader("direct:start",
+                        "select * from customer where id = ? and name = ? 
order by ID", JdbcConstants.JDBC_PARAMETERS,
+                        List.of("cust1")));
+        RuntimeExchangeException cause = 
assertIsInstanceOf(RuntimeExchangeException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("has 2 ? placeholders but the 
CamelJdbcParameters header has 1 values"),
+                cause.getMessage());
+    }
+
+    @Test
+    public void testParameterizedQueryNoNamesByteArray() throws Exception {
+        template.sendBody("direct:start", "create table blobs (id int 
auto_increment primary key, data varbinary(100))");
+
+        // a byte[] is the one value of a ? placeholder, and not one value per 
byte
+        byte[] data = new byte[] { 1, 2, 3, 4, 5 };
+        template.sendBodyAndHeader("direct:start", "insert into blobs (data) 
values (?)", JdbcConstants.JDBC_PARAMETERS,
+                data);
+
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.reset();
+        mock.expectedMessageCount(1);
+
+        template.sendBodyAndHeader("direct:start", "select data from blobs 
where data = ?", JdbcConstants.JDBC_PARAMETERS,
+                data);
+
+        MockEndpoint.assertIsSatisfied(context);
+
+        List<?> received = assertIsInstanceOf(List.class, 
mock.getReceivedExchanges().get(0).getIn().getBody());
+        assertEquals(1, received.size());
+        Map<?, ?> row = assertIsInstanceOf(Map.class, received.get(0));
+        assertArrayEquals(data, assertIsInstanceOf(byte[].class, 
row.get("DATA")));
+    }
+
+    @Test
+    public void testPlainPlaceholdersThenNamedParameters() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+
+        // the List of the first (plain ? placeholders) query stays on the 
message, and the second query (named
+        // parameters) takes its values from the headers, as the List has no 
names
+        Map<String, Object> headers = new LinkedHashMap<>();
+        headers.put(JdbcConstants.JDBC_PARAMETERS, List.of("cust1"));
+        headers.put("customer", "cust2");
+        template.sendBodyAndHeaders("direct:twoCalls", null, headers);
+
+        MockEndpoint.assertIsSatisfied(context);
+
+        List<?> received = assertIsInstanceOf(List.class, 
mock.getReceivedExchanges().get(0).getIn().getBody());
+        assertEquals(1, received.size());
+        Map<?, ?> row = assertIsInstanceOf(Map.class, received.get(0));
+        assertEquals("nsandhu", row.get("NAME"));
+    }
+
+    @Test
+    public void testParameterizedQueryNoNamesArray() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+
+        template.sendBodyAndHeader("direct:start", "select * from customer 
where id = ? and name = ? order by ID",
+                JdbcConstants.JDBC_PARAMETERS, new String[] { "cust3", 
"willem" });
+
+        MockEndpoint.assertIsSatisfied(context);
+
+        List<?> received = assertIsInstanceOf(List.class, 
mock.getReceivedExchanges().get(0).getIn().getBody());
+        assertEquals(1, received.size());
+        Map<?, ?> row = assertIsInstanceOf(Map.class, received.get(0));
+        assertEquals("willem", row.get("NAME"));
+    }
+
     @Test
     public void testParameterizedQuery() throws Exception {
         MockEndpoint mock = getMockEndpoint("mock:result");
@@ -106,6 +263,13 @@ public class JdbcParameterizedQueryTest extends 
AbstractJdbcTestSupport {
                 from("direct:start")
                         .to("jdbc:testdb?useHeadersAsParameters=true")
                         .to("mock:result");
+
+                from("direct:twoCalls")
+                        .setBody(constant("select * from customer where id = 
?"))
+                        .to("jdbc:testdb?useHeadersAsParameters=true")
+                        .setBody(constant("select * from customer where id = 
:?customer"))
+                        .to("jdbc:testdb?useHeadersAsParameters=true")
+                        .to("mock:result");
             }
         };
     }
diff --git 
a/components/camel-spring-parent/camel-spring-jdbc/src/generated/resources/META-INF/org/apache/camel/component/spring/jdbc/spring-jdbc.json
 
b/components/camel-spring-parent/camel-spring-jdbc/src/generated/resources/META-INF/org/apache/camel/component/spring/jdbc/spring-jdbc.json
index 905518eb3d89..22babf6bf36d 100644
--- 
a/components/camel-spring-parent/camel-spring-jdbc/src/generated/resources/META-INF/org/apache/camel/component/spring/jdbc/spring-jdbc.json
+++ 
b/components/camel-spring-parent/camel-spring-jdbc/src/generated/resources/META-INF/org/apache/camel/component/spring/jdbc/spring-jdbc.json
@@ -33,14 +33,14 @@
     "CamelJdbcUpdateCount": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": "int", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "If the query is an UPDATE, query the update count is 
returned in this OUT header.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_UPDATE_COUNT" },
     "CamelJdbcRowCount": { "index": 1, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": "int", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "If the query is a SELECT and the outputType is 
SelectList, the row count is returned in this OUT header.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_ROW_COUNT" },
     "CamelJdbcColumnNames": { "index": 2, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Set<String>", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The column names from the ResultSet as a 
java.util.Set type.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_COLUMN_NAMES" },
-    "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": "Map", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "A java.util.Map which has the headers to be used if 
useHeadersAsParameters has been enabled.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_PARAMETERS" },
+    "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Object", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The values of the query parameters when 
useHeadersAsParameters has been enabled: a java.util.Map with the values of the 
named parameters, or a java.util.List with the values of the positional 
placeholders, in order (a single value for [...]
     "CamelRetrieveGeneratedKeys": { "index": 4, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "defaultValue": "false", "description": "Set its value 
to true to retrieve generated keys", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_RETRIEVE_GENERATED_KEYS" },
     "CamelGeneratedColumns": { "index": 5, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"String[] or int[]", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Set it to specify the expected 
generated columns", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_COLUMNS" },
     "CamelGeneratedKeysRowCount": { "index": 6, "kind": "header", 
"displayName": "", "group": "producer", "label": "producer", "required": false, 
"javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of rows in the header that 
contains generated keys.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_ROW_COUNT" },
     "CamelGeneratedKeysRows": { "index": 7, "kind": "header", "displayName": 
"", "group": "producer", "label": "producer", "required": false, "javaType": 
"List<Map<String, Object>>", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Rows that contains the 
generated keys.", "constantName": 
"org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_DATA" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], 
"description": "The SQL statement as text, or bytes, a stream or a file of it; 
a Map or a List is not a statement: set the SQL as the body. With 
useHeadersAsParameters, the values of its placeholders come from the 
CamelJdbcParameters header, or else from the headers" },
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], 
"description": "The SQL statement as text, or bytes, a stream or a file of it; 
a Map or a List is not a statement: set the SQL as the body. With 
useHeadersAsParameters, the values of its named parameters (:?name) come from 
the CamelJdbcParameters header, or else from the headers, and the values of 
plain ? placeholders, in order, only from the CamelJdbcParameters header" },
     "producerOut": { "carriers": [ "list", "unchanged" ], "byOption": [ { 
"option": "outputType", "value": "SelectOne", "carriers": [ "map", "text", 
"value", "bytes", "pojo", "null", "unchanged" ], "javaTypeOption": 
"outputClass" }, { "option": "outputType", "value": "StreamList", "carriers": [ 
"iterator", "unchanged" ] } ], "description": "The rows of a select: a List of 
Maps of the column names and their values (beans of outputClass when set). 
SelectOne gives the Map of the one row, th [...]
   },
   "properties": {
@@ -53,7 +53,7 @@
     "resetAutoCommit": { "index": 6, "kind": "parameter", "displayName": 
"Reset Auto Commit", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": true, "description": "Camel will set 
the autoCommit on the JDBC connection to be false, commit the change after 
executed the statement and reset the autoCommit flag of the connection at the 
end, if the resetAutoCommit is true. If [...]
     "transacted": { "index": 7, "kind": "parameter", "displayName": 
"Transacted", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": true, "deprecationNote": "Not 
used: the producer does not read this option", "autowired": false, "secret": 
false, "defaultValue": false, "description": "Deprecation note: Not used: the 
producer does not read this option" },
     "useGetBytesForBlob": { "index": 8, "kind": "parameter", "displayName": 
"Use Get Bytes For Blob", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "To read BLOB 
columns as bytes (a byte) instead of a java.sql.Blob, which cannot be read 
after the producer has closed the connection. This may be needed for certain 
databases such as Oracle where yo [...]
-    "useHeadersAsParameters": { "index": 9, "kind": "parameter", 
"displayName": "Use Headers As Parameters", "group": "producer", "label": "", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Set this option to true to use the prepareStatementStrategy 
with named parameters. This allows to define queries with named placeholders, 
and use headers with the dynamic values for the query [...]
+    "useHeadersAsParameters": { "index": 9, "kind": "parameter", 
"displayName": "Use Headers As Parameters", "group": "producer", "label": "", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Set this option to true to use the prepareStatementStrategy 
with named parameters. This allows to define queries with named placeholders, 
and use headers with the dynamic values for the query [...]
     "useJDBC4ColumnNameAndLabelSemantics": { "index": 10, "kind": "parameter", 
"displayName": "Use JDBC4 Column Name And Label Semantics", "group": 
"producer", "label": "", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": true, "description": "Sets whether to use JDBC 4 or JDBC 3.0 or 
older semantic when retrieving column name. JDBC 4.0 uses columnLabel to get 
the column name where as JDBC 3.0 uses both [...]
     "lazyStartProducer": { "index": 11, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
     "beanRowMapper": { "index": 12, "kind": "parameter", "displayName": "Bean 
Row Mapper", "group": "advanced", "label": "advanced", "required": false, 
"type": "object", "javaType": "org.apache.camel.component.jdbc.BeanRowMapper", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
use a custom org.apache.camel.component.jdbc.BeanRowMapper when using 
outputClass. The default implementation will lower case the row names and skip 
underscores, and dashes. For exampl [...]
diff --git 
a/components/camel-spring-parent/camel-spring-jdbc/src/main/java/org/apache/camel/component/spring/jdbc/SpringJdbcEndpoint.java
 
b/components/camel-spring-parent/camel-spring-jdbc/src/main/java/org/apache/camel/component/spring/jdbc/SpringJdbcEndpoint.java
index be34e4e8860c..14228b9ff76e 100644
--- 
a/components/camel-spring-parent/camel-spring-jdbc/src/main/java/org/apache/camel/component/spring/jdbc/SpringJdbcEndpoint.java
+++ 
b/components/camel-spring-parent/camel-spring-jdbc/src/main/java/org/apache/camel/component/spring/jdbc/SpringJdbcEndpoint.java
@@ -35,8 +35,9 @@ import org.apache.camel.spi.UriEndpoint;
 @BodyTypes(producerIn = @BodyType(value = { Carrier.TEXT, Carrier.BYTES, 
Carrier.STREAM, Carrier.FILE },
                                   description = "The SQL statement as text, or 
bytes, a stream or a file of it; a Map or a List"
                                                 + " is not a statement: set 
the SQL as the body. With useHeadersAsParameters,"
-                                                + " the values of its 
placeholders come from the CamelJdbcParameters header, or"
-                                                + " else from the headers"),
+                                                + " the values of its named 
parameters (:?name) come from the"
+                                                + " CamelJdbcParameters 
header, or else from the headers, and the values of"
+                                                + " plain ? placeholders, in 
order, only from the CamelJdbcParameters header"),
            producerOut = @BodyType(value = { Carrier.LIST, Carrier.UNCHANGED },
                                    byOption = {
                                            @BodyType.When(option = 
"outputType", value = "SelectOne",
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 836baf23e08c..9a1ccf71dc0c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1254,6 +1254,23 @@ CamelContext of that provider. It now closes each of 
them only when no other `JC
 it returned the closed cache, whose operations throw `IllegalStateException`. 
This matters for code that calls a
 `JCacheManager` (for example one created by a custom `JCacheManagerFactory`) 
after it was closed.
 
+=== camel-jdbc, camel-spring-jdbc - plain ? placeholders take their values 
only from the CamelJdbcParameters header
+
+With `useHeadersAsParameters=true` and a query with only plain `?` 
placeholders (no named parameters `:?name`), the
+`jdbc` and `spring-jdbc` producers bound the values of all the message headers 
by position, or of the
+`CamelJdbcParameters` header when it was set. Which value went to which `?` 
then depended on the headers of the
+message and their order (which changed in 4.21 from alphabetical to the order 
they were added). The values of plain
+`?` placeholders now come only from the `CamelJdbcParameters` header: a `List` 
with the values in the order of the
+placeholders (or a `Map`, whose values are taken in the order of its entries, 
or a single value for a query with one
+`?`, such as a `String` or a `byte[]`). When the header is not set, or has 
more or fewer values than the query has
+`?` placeholders, the exchange fails with a `RuntimeExchangeException` 
(before, extra values were ignored). Set the
+values in that header, or use named parameters.
+
+Named parameters take their values from the `CamelJdbcParameters` header when 
it is a `Map`, or else from the message
+headers, as before. A `CamelJdbcParameters` header that is not a `Map` (such 
as the `List` left by an earlier query
+with plain `?` placeholders in the same route) is now ignored for named 
parameters, where before it made the exchange
+fail with `Cannot find key`.
+
 === camel-jgroups-raft - a body that does not convert to a byte array fails 
the exchange
 
 The `jgroups-raft` producer skipped a message body that has no conversion to 
`byte[]`, such as a `Map`, a POJO or a
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JdbcEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JdbcEndpointBuilderFactory.java
index d70b960c88c7..4f73a7be5cbe 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JdbcEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JdbcEndpointBuilderFactory.java
@@ -310,7 +310,9 @@ public interface JdbcEndpointBuilderFactory {
          * Set this option to true to use the prepareStatementStrategy with
          * named parameters. This allows to define queries with named
          * placeholders, and use headers with the dynamic values for the query
-         * placeholders.
+         * placeholders. A query with only positional placeholders (no named
+         * parameters) takes their values, in order, only from the
+         * CamelJdbcParameters header (a List).
          * 
          * The option is a: <code>boolean</code> type.
          * 
@@ -328,7 +330,9 @@ public interface JdbcEndpointBuilderFactory {
          * Set this option to true to use the prepareStatementStrategy with
          * named parameters. This allows to define queries with named
          * placeholders, and use headers with the dynamic values for the query
-         * placeholders.
+         * placeholders. A query with only positional placeholders (no named
+         * parameters) takes their values, in order, only from the
+         * CamelJdbcParameters header (a List).
          * 
          * The option will be converted to a <code>boolean</code> type.
          * 
@@ -659,10 +663,12 @@ public interface JdbcEndpointBuilderFactory {
             return "CamelJdbcColumnNames";
         }
         /**
-         * A java.util.Map which has the headers to be used if
-         * useHeadersAsParameters has been enabled.
+         * The values of the query parameters when useHeadersAsParameters has
+         * been enabled: a java.util.Map with the values of the named
+         * parameters, or a java.util.List with the values of the positional
+         * placeholders, in order (a single value for one placeholder).
          * 
-         * The option is a: {@code Map} type.
+         * The option is a: {@code Object} type.
          * 
          * Group: producer
          * 
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpringJdbcEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpringJdbcEndpointBuilderFactory.java
index 8a05397e4f0c..421d406a9c0a 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpringJdbcEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpringJdbcEndpointBuilderFactory.java
@@ -304,7 +304,9 @@ public interface SpringJdbcEndpointBuilderFactory {
          * Set this option to true to use the prepareStatementStrategy with
          * named parameters. This allows to define queries with named
          * placeholders, and use headers with the dynamic values for the query
-         * placeholders.
+         * placeholders. A query with only positional placeholders (no named
+         * parameters) takes their values, in order, only from the
+         * CamelJdbcParameters header (a List).
          * 
          * The option is a: <code>boolean</code> type.
          * 
@@ -322,7 +324,9 @@ public interface SpringJdbcEndpointBuilderFactory {
          * Set this option to true to use the prepareStatementStrategy with
          * named parameters. This allows to define queries with named
          * placeholders, and use headers with the dynamic values for the query
-         * placeholders.
+         * placeholders. A query with only positional placeholders (no named
+         * parameters) takes their values, in order, only from the
+         * CamelJdbcParameters header (a List).
          * 
          * The option will be converted to a <code>boolean</code> type.
          * 
@@ -656,10 +660,12 @@ public interface SpringJdbcEndpointBuilderFactory {
             return "CamelJdbcColumnNames";
         }
         /**
-         * A java.util.Map which has the headers to be used if
-         * useHeadersAsParameters has been enabled.
+         * The values of the query parameters when useHeadersAsParameters has
+         * been enabled: a java.util.Map with the values of the named
+         * parameters, or a java.util.List with the values of the positional
+         * placeholders, in order (a single value for one placeholder).
          * 
-         * The option is a: {@code Map} type.
+         * The option is a: {@code Object} type.
          * 
          * Group: producer
          * 

Reply via email to