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 f345ef6a17a6 CAMEL-26148: camel-sql - sql and sql-stored batch update
count and single Map body (#28918)
f345ef6a17a6 is described below
commit f345ef6a17a64734d31147c5ca6c3f476c7b7231
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:45:21 2026 +0100
CAMEL-26148: camel-sql - sql and sql-stored batch update count and single
Map body (#28918)
* CAMEL-26148: camel-sql - sql-stored batch update count and single Map…
… body
With batch=true, sql-stored summed the counts of executeBatch into the
CamelSqlStoredUpdateCount header, including Statement.SUCCESS_NO_INFO (-2),
so a batch of 2 items on HSQLDB reported -4. Only the known (positive)
counts are added now.
A single Map body (or CamelSqlStoredParameters header) was converted to an
Iterator of its entries, so each entry became a batch item and the call
failed with an unclear SQLException. A single Map is now one batch item.
* CAMEL-26148: camel-sql - sql batch update count and single Map body
The sql producer had the same two batch bugs as sql-stored. It summed
Statement.SUCCESS_NO_INFO (-2) into CamelSqlUpdateCount, so the header
could be negative; only the known (positive) counts are added now. A
single Map body (or CamelSqlParameters header) was iterated as its
entries, so the named parameters were not found; it is now one batch item.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../camel/catalog/components/sql-stored.json | 6 +-
.../org/apache/camel/catalog/components/sql.json | 4 +-
.../apache/camel/catalog/docs/sql-component.adoc | 4 +
.../camel/catalog/docs/sql-stored-component.adoc | 18 ++
.../org/apache/camel/component/sql/sql.json | 4 +-
.../camel/component/sql/stored/sql-stored.json | 6 +-
.../camel-sql/src/main/docs/sql-component.adoc | 4 +
.../src/main/docs/sql-stored-component.adoc | 18 ++
.../apache/camel/component/sql/SqlConstants.java | 5 +-
.../apache/camel/component/sql/SqlEndpoint.java | 4 +-
.../apache/camel/component/sql/SqlProducer.java | 20 ++-
.../component/sql/stored/SqlStoredConstants.java | 6 +-
.../component/sql/stored/SqlStoredEndpoint.java | 8 +-
.../component/sql/stored/SqlStoredProducer.java | 21 ++-
.../camel/component/sql/SqlProducerBatchTest.java | 198 +++++++++++++++++++++
.../component/sql/stored/ProducerBatchTest.java | 44 +++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 12 ++
.../endpoint/dsl/SqlEndpointBuilderFactory.java | 5 +-
.../dsl/SqlStoredEndpointBuilderFactory.java | 5 +-
19 files changed, 365 insertions(+), 27 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/sql-stored.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/sql-stored.json
index 16606cd4dc34..a7abb1450ff7 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/sql-stored.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/sql-stored.json
@@ -33,11 +33,11 @@
"headers": {
"CamelSqlStoredTemplate": { "index": 0, "kind": "header", "displayName":
"", "group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "description": "The stored-procedure template to execute. This
header is ignored unless the endpoint enables allowTemplateFromHeader=true
(disabled by default); when enabled it overrides the endpoint-configured
template.", "constantName": "org.apa [...]
"CamelSqlStoredParameters": { "index": 1, "kind": "header", "displayName":
"", "group": "producer", "label": "producer", "required": false, "javaType":
"Iterator", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "description": "The parameters", "constantName":
"org.apache.camel.component.sql.stored.SqlStoredConstants#SQL_STORED_PARAMETERS"
},
- "CamelSqlStoredUpdateCount": { "index": 2, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "important": true, "description": "The update count",
"constantName":
"org.apache.camel.component.sql.stored.SqlStoredConstants#SQL_STORED_UPDATE_COUNT"
}
+ "CamelSqlStoredUpdateCount": { "index": 2, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "important": true, "description": "The update count.
With batch=true, the sum of the update counts of the batch items; an item the
JDBC driver returns no count for (Statement.SUCCESS_NO_INFO) is not counted, so
the value can be lower than the [...]
},
"bodyTypes": {
- "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": "batch",
"value": "true", "carriers": [ "list", "iterator" ] }, { "option":
"useMessageBodyForTemplate", "value": "true", "carriers": [ "text" ] } ],
"description": "What the expressions of the IN parameters read: a :#name
parameter takes its value from a Map body, or else from the header. With batch,
a List or an Iterator of such bodies, one batch item each (a single Map is not
a batch); with useMessageBodyForTemplate, [...]
- "producerOut": { "carriers": [ "map" ], "javaType": "java.util.Map<String,
Object>", "dependsOn": [ "outputHeader" ], "byOption": [ { "option": "noop",
"value": "true", "carriers": [ "unchanged" ] }, { "option": "batch", "value":
"true", "carriers": [ "unchanged" ] } ], "description": "A Map of the names of
the OUT and INOUT parameters and their values, with a result set the procedure
returns under #result-set-1 (a List of Maps) and the update count under
#update-count-1, which is al [...]
+ "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": "batch",
"value": "true", "carriers": [ "list", "iterator", "map" ] }, { "option":
"useMessageBodyForTemplate", "value": "true", "carriers": [ "text" ] } ],
"description": "What the expressions of the IN parameters read: a :#name
parameter takes its value from a Map body, or else from the header. With batch,
a List or an Iterator of such bodies, one batch item each (a single Map is one
batch item); with useMessageBodyFo [...]
+ "producerOut": { "carriers": [ "map" ], "javaType": "java.util.Map<String,
Object>", "dependsOn": [ "outputHeader" ], "byOption": [ { "option": "noop",
"value": "true", "carriers": [ "unchanged" ] }, { "option": "batch", "value":
"true", "carriers": [ "unchanged" ] } ], "description": "A Map of the names of
the OUT and INOUT parameters and their values, with a result set the procedure
returns under #result-set-1 (a List of Maps) and the update count under
#update-count-1, which is al [...]
},
"properties": {
"template": { "index": 0, "kind": "path", "displayName": "Template",
"group": "producer", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "supportFileReference": true,
"largeInput": true, "inputLanguage": "sql", "description": "Sets the stored
procedure template to perform. You can externalize the template by using file:
or classpath: as prefix and specify the location of [...]
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/sql.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/sql.json
index 1c905fbc57cb..4641f8fd3aec 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/sql.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/sql.json
@@ -39,7 +39,7 @@
},
"headers": {
"CamelSqlQuery": { "index": 0, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "description": "Query to execute. This header is ignored
unless the endpoint enables allowQueryFromHeader=true (disabled by default);
when enabled it takes precedence over the query specified in the endpoint URI.
Note that query parameters in the header ar [...]
- "CamelSqlUpdateCount": { "index": 1, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"Integer", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "important": true, "description": "The number of rows updated
for update operations, returned as an Integer object. This header is not
provided when using outputType=StreamList.", "constantName":
"org.apache.camel.component.sql.SqlConstants#SQL_UPDA [...]
+ "CamelSqlUpdateCount": { "index": 1, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"Integer", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "important": true, "description": "The number of rows updated
for update operations, returned as an Integer object. This header is not
provided when using outputType=StreamList. With batch=true, the sum of the
update counts of the batch items; an it [...]
"CamelSqlRowCount": { "index": 2, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"Integer", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "important": true, "description": "The number of rows returned
for select operations, returned as an Integer object. This header is not
provided when using outputType=StreamList.", "constantName":
"org.apache.camel.component.sql.SqlConstants#SQL_ROW_COUNT" },
"CamelSqlRetrieveGeneratedKeys": { "index": 3, "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.sql.SqlConstants#SQL_RETRIEVE_GENERATED_KEYS" },
"CamelSqlGeneratedColumns": { "index": 4, "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.sql.SqlConstants#SQL_GENERATED_COLUMNS" },
@@ -50,7 +50,7 @@
},
"bodyTypes": {
"consumerOut": { "carriers": [ "map" ], "javaType": "java.util.Map<String,
Object>", "dependsOn": [ "outputHeader", "useIterator", "outputType" ],
"byOption": [ { "option": "useIterator", "value": "true", "carriers": [ "map"
], "javaType": "java.util.Map<String, Object>", "javaTypeOption": "outputClass"
}, { "option": "useIterator", "value": "false", "carriers": [ "list" ] }, {
"option": "outputType", "value": "SelectList", "carriers": [ "map" ],
"javaType": "java.util.Map<String, Ob [...]
- "producerIn": { "carriers": [ "any" ], "description": "The parameters of
the query: a Map for the named ones, a List or an array for the # ones; the
query itself with useMessageBodyForSql" },
+ "producerIn": { "carriers": [ "any" ], "description": "The parameters of
the query: a Map for the named ones, a List or an array for the # ones; the
query itself with useMessageBodyForSql. With batch, a List or an Iterator of
such parameters, one batch item each (a single Map is one batch item)" },
"producerOut": { "carriers": [ "list", "unchanged" ], "dependsOn": [
"outputHeader", "noop" ], "byOption": [ { "option": "outputType", "value":
"SelectOne", "carriers": [ "map", "value", "null" ], "javaTypeOption":
"outputClass" }, { "option": "outputType", "value": "StreamList", "carriers": [
"iterator" ] } ], "description": "The rows of a select: a List of Maps of the
column names and their values (objects of outputClass when set). A query that
is not a select leaves the body uncha [...]
},
"properties": {
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sql-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sql-component.adoc
index f48fed0af171..225964f01b22 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sql-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sql-component.adoc
@@ -139,6 +139,10 @@ If `batch` is set to `true`, then the interpretation of
the inbound
message body changes slightly – instead of an iterator of parameters,
the component expects an iterator that contains the parameter iterators;
the size of the outer iterator determines the batch size.
+A single `Map` body (named parameters) is one batch item.
+The `CamelSqlUpdateCount` header holds the sum of the update counts of the
batch items.
+A JDBC driver may not know the update count of an item and return
`Statement.SUCCESS_NO_INFO` instead;
+such an item is not counted, so the header can be lower than the number of
rows updated, or `0`.
You can use the option `useMessageBodyForSql` that
allows to use the message body as the SQL statement, and then the SQL
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sql-stored-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sql-stored-component.adoc
index b41a11269a7c..6dd4404ca94a 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sql-stored-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sql-stored-component.adoc
@@ -221,6 +221,24 @@ You can configure query timeout (via
`template.queryTimeout`) on statements used
This will be overridden by the remaining transaction timeout when executing
within a transaction that has a timeout specified at the transaction level.
+==== Batch
+
+With `batch=true` the stored procedure is called once for each batch item, and
all calls are sent to the database as one JDBC batch.
+The batch items are taken from the message body (or from the
`CamelSqlStoredParameters` header when `useMessageBodyForTemplate=true`):
+
+* a `List` or an `Iterator`, where each element is one batch item, typically a
`Map` with the values of the IN parameters.
+* a single `Map`, which is one batch item.
+
+._XML-only: stored procedure call in batch mode_
+[source,xml]
+----
+<to uri="sql-stored:BATCHFN(INTEGER :#num)?batch=true"/>
+----
+
+The message body is not changed, and the `CamelSqlStoredUpdateCount` header
holds the sum of the update counts of the batch items.
+A JDBC driver may not know the update count of an item and return
`Statement.SUCCESS_NO_INFO` instead (HSQLDB does this for stored procedure
calls).
+Such an item is not counted, so the header can be lower than the number of
rows updated, or `0`.
+
=== Camel SQL Starter
diff --git
a/components/camel-sql/src/generated/resources/META-INF/org/apache/camel/component/sql/sql.json
b/components/camel-sql/src/generated/resources/META-INF/org/apache/camel/component/sql/sql.json
index 1c905fbc57cb..4641f8fd3aec 100644
---
a/components/camel-sql/src/generated/resources/META-INF/org/apache/camel/component/sql/sql.json
+++
b/components/camel-sql/src/generated/resources/META-INF/org/apache/camel/component/sql/sql.json
@@ -39,7 +39,7 @@
},
"headers": {
"CamelSqlQuery": { "index": 0, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "description": "Query to execute. This header is ignored
unless the endpoint enables allowQueryFromHeader=true (disabled by default);
when enabled it takes precedence over the query specified in the endpoint URI.
Note that query parameters in the header ar [...]
- "CamelSqlUpdateCount": { "index": 1, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"Integer", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "important": true, "description": "The number of rows updated
for update operations, returned as an Integer object. This header is not
provided when using outputType=StreamList.", "constantName":
"org.apache.camel.component.sql.SqlConstants#SQL_UPDA [...]
+ "CamelSqlUpdateCount": { "index": 1, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"Integer", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "important": true, "description": "The number of rows updated
for update operations, returned as an Integer object. This header is not
provided when using outputType=StreamList. With batch=true, the sum of the
update counts of the batch items; an it [...]
"CamelSqlRowCount": { "index": 2, "kind": "header", "displayName": "",
"group": "producer", "label": "producer", "required": false, "javaType":
"Integer", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "important": true, "description": "The number of rows returned
for select operations, returned as an Integer object. This header is not
provided when using outputType=StreamList.", "constantName":
"org.apache.camel.component.sql.SqlConstants#SQL_ROW_COUNT" },
"CamelSqlRetrieveGeneratedKeys": { "index": 3, "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.sql.SqlConstants#SQL_RETRIEVE_GENERATED_KEYS" },
"CamelSqlGeneratedColumns": { "index": 4, "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.sql.SqlConstants#SQL_GENERATED_COLUMNS" },
@@ -50,7 +50,7 @@
},
"bodyTypes": {
"consumerOut": { "carriers": [ "map" ], "javaType": "java.util.Map<String,
Object>", "dependsOn": [ "outputHeader", "useIterator", "outputType" ],
"byOption": [ { "option": "useIterator", "value": "true", "carriers": [ "map"
], "javaType": "java.util.Map<String, Object>", "javaTypeOption": "outputClass"
}, { "option": "useIterator", "value": "false", "carriers": [ "list" ] }, {
"option": "outputType", "value": "SelectList", "carriers": [ "map" ],
"javaType": "java.util.Map<String, Ob [...]
- "producerIn": { "carriers": [ "any" ], "description": "The parameters of
the query: a Map for the named ones, a List or an array for the # ones; the
query itself with useMessageBodyForSql" },
+ "producerIn": { "carriers": [ "any" ], "description": "The parameters of
the query: a Map for the named ones, a List or an array for the # ones; the
query itself with useMessageBodyForSql. With batch, a List or an Iterator of
such parameters, one batch item each (a single Map is one batch item)" },
"producerOut": { "carriers": [ "list", "unchanged" ], "dependsOn": [
"outputHeader", "noop" ], "byOption": [ { "option": "outputType", "value":
"SelectOne", "carriers": [ "map", "value", "null" ], "javaTypeOption":
"outputClass" }, { "option": "outputType", "value": "StreamList", "carriers": [
"iterator" ] } ], "description": "The rows of a select: a List of Maps of the
column names and their values (objects of outputClass when set). A query that
is not a select leaves the body uncha [...]
},
"properties": {
diff --git
a/components/camel-sql/src/generated/resources/META-INF/org/apache/camel/component/sql/stored/sql-stored.json
b/components/camel-sql/src/generated/resources/META-INF/org/apache/camel/component/sql/stored/sql-stored.json
index 16606cd4dc34..a7abb1450ff7 100644
---
a/components/camel-sql/src/generated/resources/META-INF/org/apache/camel/component/sql/stored/sql-stored.json
+++
b/components/camel-sql/src/generated/resources/META-INF/org/apache/camel/component/sql/stored/sql-stored.json
@@ -33,11 +33,11 @@
"headers": {
"CamelSqlStoredTemplate": { "index": 0, "kind": "header", "displayName":
"", "group": "producer", "label": "producer", "required": false, "javaType":
"String", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "description": "The stored-procedure template to execute. This
header is ignored unless the endpoint enables allowTemplateFromHeader=true
(disabled by default); when enabled it overrides the endpoint-configured
template.", "constantName": "org.apa [...]
"CamelSqlStoredParameters": { "index": 1, "kind": "header", "displayName":
"", "group": "producer", "label": "producer", "required": false, "javaType":
"Iterator", "deprecated": false, "deprecationNote": "", "autowired": false,
"secret": false, "description": "The parameters", "constantName":
"org.apache.camel.component.sql.stored.SqlStoredConstants#SQL_STORED_PARAMETERS"
},
- "CamelSqlStoredUpdateCount": { "index": 2, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "important": true, "description": "The update count",
"constantName":
"org.apache.camel.component.sql.stored.SqlStoredConstants#SQL_STORED_UPDATE_COUNT"
}
+ "CamelSqlStoredUpdateCount": { "index": 2, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "important": true, "description": "The update count.
With batch=true, the sum of the update counts of the batch items; an item the
JDBC driver returns no count for (Statement.SUCCESS_NO_INFO) is not counted, so
the value can be lower than the [...]
},
"bodyTypes": {
- "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": "batch",
"value": "true", "carriers": [ "list", "iterator" ] }, { "option":
"useMessageBodyForTemplate", "value": "true", "carriers": [ "text" ] } ],
"description": "What the expressions of the IN parameters read: a :#name
parameter takes its value from a Map body, or else from the header. With batch,
a List or an Iterator of such bodies, one batch item each (a single Map is not
a batch); with useMessageBodyForTemplate, [...]
- "producerOut": { "carriers": [ "map" ], "javaType": "java.util.Map<String,
Object>", "dependsOn": [ "outputHeader" ], "byOption": [ { "option": "noop",
"value": "true", "carriers": [ "unchanged" ] }, { "option": "batch", "value":
"true", "carriers": [ "unchanged" ] } ], "description": "A Map of the names of
the OUT and INOUT parameters and their values, with a result set the procedure
returns under #result-set-1 (a List of Maps) and the update count under
#update-count-1, which is al [...]
+ "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": "batch",
"value": "true", "carriers": [ "list", "iterator", "map" ] }, { "option":
"useMessageBodyForTemplate", "value": "true", "carriers": [ "text" ] } ],
"description": "What the expressions of the IN parameters read: a :#name
parameter takes its value from a Map body, or else from the header. With batch,
a List or an Iterator of such bodies, one batch item each (a single Map is one
batch item); with useMessageBodyFo [...]
+ "producerOut": { "carriers": [ "map" ], "javaType": "java.util.Map<String,
Object>", "dependsOn": [ "outputHeader" ], "byOption": [ { "option": "noop",
"value": "true", "carriers": [ "unchanged" ] }, { "option": "batch", "value":
"true", "carriers": [ "unchanged" ] } ], "description": "A Map of the names of
the OUT and INOUT parameters and their values, with a result set the procedure
returns under #result-set-1 (a List of Maps) and the update count under
#update-count-1, which is al [...]
},
"properties": {
"template": { "index": 0, "kind": "path", "displayName": "Template",
"group": "producer", "label": "", "required": true, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "supportFileReference": true,
"largeInput": true, "inputLanguage": "sql", "description": "Sets the stored
procedure template to perform. You can externalize the template by using file:
or classpath: as prefix and specify the location of [...]
diff --git a/components/camel-sql/src/main/docs/sql-component.adoc
b/components/camel-sql/src/main/docs/sql-component.adoc
index f48fed0af171..225964f01b22 100644
--- a/components/camel-sql/src/main/docs/sql-component.adoc
+++ b/components/camel-sql/src/main/docs/sql-component.adoc
@@ -139,6 +139,10 @@ If `batch` is set to `true`, then the interpretation of
the inbound
message body changes slightly – instead of an iterator of parameters,
the component expects an iterator that contains the parameter iterators;
the size of the outer iterator determines the batch size.
+A single `Map` body (named parameters) is one batch item.
+The `CamelSqlUpdateCount` header holds the sum of the update counts of the
batch items.
+A JDBC driver may not know the update count of an item and return
`Statement.SUCCESS_NO_INFO` instead;
+such an item is not counted, so the header can be lower than the number of
rows updated, or `0`.
You can use the option `useMessageBodyForSql` that
allows to use the message body as the SQL statement, and then the SQL
diff --git a/components/camel-sql/src/main/docs/sql-stored-component.adoc
b/components/camel-sql/src/main/docs/sql-stored-component.adoc
index b41a11269a7c..6dd4404ca94a 100644
--- a/components/camel-sql/src/main/docs/sql-stored-component.adoc
+++ b/components/camel-sql/src/main/docs/sql-stored-component.adoc
@@ -221,6 +221,24 @@ You can configure query timeout (via
`template.queryTimeout`) on statements used
This will be overridden by the remaining transaction timeout when executing
within a transaction that has a timeout specified at the transaction level.
+==== Batch
+
+With `batch=true` the stored procedure is called once for each batch item, and
all calls are sent to the database as one JDBC batch.
+The batch items are taken from the message body (or from the
`CamelSqlStoredParameters` header when `useMessageBodyForTemplate=true`):
+
+* a `List` or an `Iterator`, where each element is one batch item, typically a
`Map` with the values of the IN parameters.
+* a single `Map`, which is one batch item.
+
+._XML-only: stored procedure call in batch mode_
+[source,xml]
+----
+<to uri="sql-stored:BATCHFN(INTEGER :#num)?batch=true"/>
+----
+
+The message body is not changed, and the `CamelSqlStoredUpdateCount` header
holds the sum of the update counts of the batch items.
+A JDBC driver may not know the update count of an item and return
`Statement.SUCCESS_NO_INFO` instead (HSQLDB does this for stored procedure
calls).
+Such an item is not counted, so the header can be lower than the number of
rows updated, or `0`.
+
=== Camel SQL Starter
diff --git
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConstants.java
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConstants.java
index 2e961586aaa5..5d8a3fee31b9 100644
---
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConstants.java
+++
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConstants.java
@@ -32,7 +32,10 @@ public final class SqlConstants {
public static final String SQL_QUERY = "CamelSqlQuery";
@Metadata(label = "producer", description = "The number of rows updated
for `update` operations, returned as an" +
" `Integer` object. This
header is not provided when using" +
- " outputType=StreamList.",
+ " outputType=StreamList. With
batch=true, the sum of the update counts" +
+ " of the batch items; an item
the JDBC driver returns no count for" +
+ " (Statement.SUCCESS_NO_INFO)
is not counted, so the value can be lower" +
+ " than the number of rows
updated, or 0.",
javaType = "Integer", important = true)
public static final String SQL_UPDATE_COUNT = "CamelSqlUpdateCount";
@Metadata(label = "producer", description = "The number of rows returned
for `select` operations, returned as an" +
diff --git
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlEndpoint.java
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlEndpoint.java
index 465887263818..d8a7c6039768 100644
---
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlEndpoint.java
+++
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlEndpoint.java
@@ -54,7 +54,9 @@ import org.apache.camel.util.UnsafeUriCharactersEncoder;
+ " object of outputClass;
with useIterator=false the List of them"),
producerIn = @BodyType(value = Carrier.ANY,
description = "The parameters of the query:
a Map for the named ones, a List or an array"
- + " for the # ones; the query
itself with useMessageBodyForSql"),
+ + " for the # ones; the query
itself with useMessageBodyForSql. With batch,"
+ + " a List or an Iterator of
such parameters, one batch item each (a single"
+ + " Map is one batch item)"),
producerOut = @BodyType(value = { Carrier.LIST, Carrier.UNCHANGED
}, dependsOn = { "outputHeader", "noop" },
byOption = {
@BodyType.When(option =
"outputType", value = "SelectOne",
diff --git
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlProducer.java
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlProducer.java
index b2e7b7d143bd..c85c6379b3ec 100644
---
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlProducer.java
+++
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlProducer.java
@@ -25,6 +25,7 @@ import java.sql.Statement;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import javax.sql.DataSource;
@@ -171,7 +172,11 @@ public class SqlProducer extends DefaultProducer {
int[] updateCounts = ps.executeBatch();
int total = 0;
for (int count : updateCounts) {
- total += count;
+ // a driver may return a negative value such
as Statement.SUCCESS_NO_INFO (-2)
+ // when the count of an item is unknown, so
only add the known counts
+ if (count > 0) {
+ total += count;
+ }
}
exchange.getIn().setHeader(SqlConstants.SQL_UPDATE_COUNT, total);
if (!exchange.isTransacted() && manualCommit) {
@@ -302,11 +307,18 @@ public class SqlProducer extends DefaultProducer {
if (alwaysPopulateStatement || expected > 0) {
// transfer incoming message body data to prepared statement
parameters, if necessary
if (batch) {
- Iterator<?> iterator;
+ Object items;
if (useMessageBodyForSql) {
- iterator =
exchange.getIn().getHeader(SqlConstants.SQL_PARAMETERS, Iterator.class);
+ items =
exchange.getIn().getHeader(SqlConstants.SQL_PARAMETERS);
+ } else {
+ items = exchange.getIn().getBody();
+ }
+ Iterator<?> iterator;
+ if (items instanceof Map) {
+ // a single Map is one batch item (converting it to an
Iterator would iterate its entries)
+ iterator = Collections.singletonList(items).iterator();
} else {
- iterator = exchange.getIn().getBody(Iterator.class);
+ iterator =
exchange.getContext().getTypeConverter().convertTo(Iterator.class, exchange,
items);
}
while (iterator != null && iterator.hasNext()) {
Object value = iterator.next();
diff --git
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/stored/SqlStoredConstants.java
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/stored/SqlStoredConstants.java
index a8f91ff518da..576983f52e77 100644
---
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/stored/SqlStoredConstants.java
+++
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/stored/SqlStoredConstants.java
@@ -27,7 +27,11 @@ public final class SqlStoredConstants {
public static final String SQL_STORED_TEMPLATE = "CamelSqlStoredTemplate";
@Metadata(label = "producer", description = "The parameters", javaType =
"Iterator")
public static final String SQL_STORED_PARAMETERS =
"CamelSqlStoredParameters";
- @Metadata(label = "producer", description = "The update count", javaType =
"Integer", important = true)
+ @Metadata(label = "producer",
+ description = "The update count. With batch=true, the sum of the
update counts of the batch items; an item"
+ + " the JDBC driver returns no count for
(Statement.SUCCESS_NO_INFO) is not counted, so the"
+ + " value can be lower than the number of rows
updated, or 0.",
+ javaType = "Integer", important = true)
public static final String SQL_STORED_UPDATE_COUNT =
"CamelSqlStoredUpdateCount";
private SqlStoredConstants() {
diff --git
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/stored/SqlStoredEndpoint.java
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/stored/SqlStoredEndpoint.java
index d1628d12f490..fdc5414db542 100644
---
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/stored/SqlStoredEndpoint.java
+++
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/stored/SqlStoredEndpoint.java
@@ -50,13 +50,13 @@ import org.springframework.jdbc.core.JdbcTemplate;
@BodyTypes(producerIn = @BodyType(value = Carrier.ANY,
byOption = {
@BodyType.When(option = "batch",
value = "true",
- carriers = {
Carrier.LIST, Carrier.ITERATOR }),
+ carriers = {
Carrier.LIST, Carrier.ITERATOR, Carrier.MAP }),
@BodyType.When(option =
"useMessageBodyForTemplate", value = "true",
carriers =
Carrier.TEXT) },
description = "What the expressions of the
IN parameters read: a :#name parameter takes"
+ " its value from a Map body,
or else from the header. With batch, a List or"
- + " an Iterator of such
bodies, one batch item each (a single Map is not a"
- + " batch); with
useMessageBodyForTemplate, the template as text, with the"
+ + " an Iterator of such
bodies, one batch item each (a single Map is one"
+ + " batch item); with
useMessageBodyForTemplate, the template as text, with the"
+ " parameters in the
CamelSqlStoredParameters header"),
producerOut = @BodyType(value = Carrier.MAP, javaType =
"java.util.Map<String, Object>",
dependsOn = "outputHeader",
@@ -68,7 +68,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
+ " and the update count
under #update-count-1, which is also in the"
+ " CamelSqlStoredUpdateCount
header. With outputHeader the Map is in that"
+ " header and the body is
unchanged; batch leaves the body unchanged, with"
- + " the total update count in
the header"))
+ + " the total of the known
update counts in the header"))
public class SqlStoredEndpoint extends DefaultEndpoint implements
EndpointServiceLocation {
private CallableStatementWrapperFactory wrapperFactory;
diff --git
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/stored/SqlStoredProducer.java
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/stored/SqlStoredProducer.java
index 67b5404ed352..3946a76751d0 100644
---
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/stored/SqlStoredProducer.java
+++
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/stored/SqlStoredProducer.java
@@ -17,7 +17,9 @@
package org.apache.camel.component.sql.stored;
import java.sql.SQLException;
+import java.util.Collections;
import java.util.Iterator;
+import java.util.Map;
import org.apache.camel.Exchange;
import org.apache.camel.component.sql.SqlHelper;
@@ -46,11 +48,18 @@ public class SqlStoredProducer extends DefaultProducer {
public void execute(StatementWrapper ps) throws SQLException,
DataAccessException {
// transfer incoming message body data to prepared statement
parameters, if necessary
if (getEndpoint().isBatch()) {
- Iterator<?> iterator;
+ Object items;
if (getEndpoint().isUseMessageBodyForTemplate()) {
- iterator =
exchange.getIn().getHeader(SqlStoredConstants.SQL_STORED_PARAMETERS,
Iterator.class);
+ items =
exchange.getIn().getHeader(SqlStoredConstants.SQL_STORED_PARAMETERS);
} else {
- iterator = exchange.getIn().getBody(Iterator.class);
+ items = exchange.getIn().getBody();
+ }
+ Iterator<?> iterator;
+ if (items instanceof Map) {
+ // a single Map is one batch item (converting it to an
Iterator would iterate its entries)
+ iterator = Collections.singletonList(items).iterator();
+ } else {
+ iterator =
exchange.getContext().getTypeConverter().convertTo(Iterator.class, exchange,
items);
}
if (iterator == null) {
@@ -75,7 +84,11 @@ public class SqlStoredProducer extends DefaultProducer {
int[] updateCounts = ps.executeBatch();
int total = 0;
for (int count : updateCounts) {
- total += count;
+ // a driver may return a negative value such as
Statement.SUCCESS_NO_INFO (-2)
+ // when the count of an item is unknown, so only add
the known counts
+ if (count > 0) {
+ total += count;
+ }
}
exchange.getIn().setHeader(SqlStoredConstants.SQL_STORED_UPDATE_COUNT, total);
} else {
diff --git
a/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlProducerBatchTest.java
b/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlProducerBatchTest.java
new file mode 100644
index 000000000000..58d130f33090
--- /dev/null
+++
b/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlProducerBatchTest.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.sql;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DelegatingDataSource;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class SqlProducerBatchTest extends CamelTestSupport {
+
+ private EmbeddedDatabase db;
+ private JdbcTemplate jdbcTemplate;
+
+ @Override
+ public void doPreSetup() throws Exception {
+ db = new EmbeddedDatabaseBuilder()
+ .setName(getClass().getSimpleName())
+ .setType(EmbeddedDatabaseType.H2)
+ .addScript("sql/createAndPopulateDatabase.sql").build();
+
+ jdbcTemplate = new JdbcTemplate(db);
+ }
+
+ @Override
+ public void doPostTearDown() throws Exception {
+ if (db != null) {
+ db.shutdown();
+ }
+ }
+
+ @Test
+ public void testBatchUpdateCount() {
+ List<Map<String, Object>> data = Arrays.asList(project(4, "Donald"),
project(5, "Goofy"));
+
+ Exchange out = template.request("direct:batch", e ->
e.getIn().setBody(data));
+
+ assertEquals(2,
out.getMessage().getHeader(SqlConstants.SQL_UPDATE_COUNT, Integer.class));
+ assertEquals(5, jdbcTemplate.queryForObject("select count(*) from
projects", Integer.class));
+ }
+
+ @Test
+ public void testBatchUpdateCountWithSuccessNoInfo() {
+ List<Map<String, Object>> data = Arrays.asList(project(4, "Donald"),
project(5, "Goofy"));
+
+ Exchange out = template.request("direct:noInfo", e ->
e.getIn().setBody(data));
+
+ // the driver returns Statement.SUCCESS_NO_INFO (-2) for each item,
which must not make the total negative
+ assertEquals(0,
out.getMessage().getHeader(SqlConstants.SQL_UPDATE_COUNT, Integer.class));
+ assertEquals(5, jdbcTemplate.queryForObject("select count(*) from
projects", Integer.class));
+ }
+
+ @Test
+ public void testBatchSingleMapBodyIsOneBatchItem() {
+ Exchange out = template.request("direct:batch", e ->
e.getIn().setBody(project(4, "Donald")));
+
+ assertNull(out.getException());
+ assertEquals(1,
out.getMessage().getHeader(SqlConstants.SQL_UPDATE_COUNT, Integer.class));
+ assertEquals("Donald", jdbcTemplate.queryForObject("select project
from projects where id = 4", String.class));
+ assertEquals(4, jdbcTemplate.queryForObject("select count(*) from
projects", Integer.class));
+ }
+
+ @Test
+ public void testBatchSingleMapHeaderIsOneBatchItem() {
+ Exchange out = template.request("direct:body", e -> {
+ e.getIn().setBody("insert into projects values (:?id, :?project,
:?license)");
+ e.getIn().setHeader(SqlConstants.SQL_PARAMETERS, project(4,
"Donald"));
+ });
+
+ assertNull(out.getException());
+ assertEquals(1,
out.getMessage().getHeader(SqlConstants.SQL_UPDATE_COUNT, Integer.class));
+ assertEquals("Donald", jdbcTemplate.queryForObject("select project
from projects where id = 4", String.class));
+ assertEquals(4, jdbcTemplate.queryForObject("select count(*) from
projects", Integer.class));
+ }
+
+ private static Map<String, Object> project(int id, String project) {
+ Map<String, Object> map = new HashMap<>();
+ map.put("id", id);
+ map.put("project", project);
+ map.put("license", "DIS");
+ return map;
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ getContext().getComponent("sql",
SqlComponent.class).setDataSource(db);
+ getContext().getRegistry().bind("noInfoDataSource", new
SuccessNoInfoDataSource(db));
+
+ from("direct:batch")
+ .to("sql:insert into projects values (:#id, :#project,
:#license)?batch=true");
+
+ from("direct:noInfo")
+ .to("sql:insert into projects values (:#id, :#project,
:#license)?batch=true"
+ + "&dataSource=#noInfoDataSource");
+
+ from("direct:body")
+ .to("sql:query?batch=true&useMessageBodyForSql=true");
+ }
+ };
+ }
+
+ /**
+ * A data source whose prepared statements return
Statement.SUCCESS_NO_INFO for each batch item, as some JDBC
+ * drivers do.
+ */
+ private static final class SuccessNoInfoDataSource extends
DelegatingDataSource {
+
+ SuccessNoInfoDataSource(DataSource target) {
+ super(target);
+ }
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ Connection con = super.getConnection();
+ return (Connection)
Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[] {
Connection.class },
+ new Delegate(con) {
+ @Override
+ Object invoke(Method method, Object result) {
+ if (result instanceof PreparedStatement ps) {
+ return
Proxy.newProxyInstance(getClass().getClassLoader(),
+ new Class<?>[] {
PreparedStatement.class }, new Delegate(ps) {
+ @Override
+ Object invoke(Method method,
Object result) {
+ if
("executeBatch".equals(method.getName())) {
+ int[] counts = new
int[((int[]) result).length];
+ Arrays.fill(counts,
Statement.SUCCESS_NO_INFO);
+ return counts;
+ }
+ return result;
+ }
+ });
+ }
+ return result;
+ }
+ });
+ }
+ }
+
+ private abstract static class Delegate implements InvocationHandler {
+
+ private final Object target;
+
+ Delegate(Object target) {
+ this.target = target;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
+ try {
+ return invoke(method, method.invoke(target, args));
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ }
+
+ abstract Object invoke(Method method, Object result);
+ }
+}
diff --git
a/components/camel-sql/src/test/java/org/apache/camel/component/sql/stored/ProducerBatchTest.java
b/components/camel-sql/src/test/java/org/apache/camel/component/sql/stored/ProducerBatchTest.java
index 6bb13ffa3136..89b507fc7f35 100644
---
a/components/camel-sql/src/test/java/org/apache/camel/component/sql/stored/ProducerBatchTest.java
+++
b/components/camel-sql/src/test/java/org/apache/camel/component/sql/stored/ProducerBatchTest.java
@@ -81,6 +81,47 @@ public class ProducerBatchTest extends CamelTestSupport {
assertNotNull(exchange.getIn().getHeader(SqlStoredConstants.SQL_STORED_UPDATE_COUNT));
assertEquals(batchfnCallsBefore + 2,
TestStoredProcedure.BATCHFN_CALL_COUNTER.get());
+ // HSQLDB returns Statement.SUCCESS_NO_INFO (-2) for each item, which
must not make the total negative
+ assertEquals(0,
exchange.getIn().getHeader(SqlStoredConstants.SQL_STORED_UPDATE_COUNT,
Integer.class));
+ }
+
+ @Test
+ public void shouldExecuteSingleMapBodyAsOneBatchItem() throws
InterruptedException {
+ MockEndpoint mock = getMockEndpoint("mock:query");
+ mock.expectedMessageCount(1);
+
+ Map<String, Object> batch1 = new HashMap<>();
+ batch1.put("num", "1");
+ batch1.put("other", "2");
+
+ final long batchfnCallsBefore =
TestStoredProcedure.BATCHFN_CALL_COUNTER.get();
+
+ template.requestBody("direct:query", batch1);
+
+ MockEndpoint.assertIsSatisfied(context);
+
+ Exchange exchange = mock.getExchanges().get(0);
+ assertEquals(batchfnCallsBefore + 1,
TestStoredProcedure.BATCHFN_CALL_COUNTER.get());
+ assertEquals(0,
exchange.getIn().getHeader(SqlStoredConstants.SQL_STORED_UPDATE_COUNT,
Integer.class));
+ }
+
+ @Test
+ public void shouldExecuteSingleMapHeaderAsOneBatchItem() throws
InterruptedException {
+ MockEndpoint mock = getMockEndpoint("mock:template");
+ mock.expectedMessageCount(1);
+
+ Map<String, Object> batch1 = new HashMap<>();
+ batch1.put("num", "1");
+ batch1.put("other", "2");
+
+ final long batchfnCallsBefore =
TestStoredProcedure.BATCHFN_CALL_COUNTER.get();
+
+ template.requestBodyAndHeader("direct:template", "BATCHFN(INTEGER
:#num)",
+ SqlStoredConstants.SQL_STORED_PARAMETERS, batch1);
+
+ MockEndpoint.assertIsSatisfied(context);
+
+ assertEquals(batchfnCallsBefore + 1,
TestStoredProcedure.BATCHFN_CALL_COUNTER.get());
}
@Override
@@ -92,6 +133,9 @@ public class ProducerBatchTest extends CamelTestSupport {
getContext().getComponent("sql-stored",
SqlStoredComponent.class).setDataSource(db);
from("direct:query").to("sql-stored:BATCHFN(INTEGER
:#num)?batch=true").to("mock:query");
+
+
from("direct:template").to("sql-stored:template?batch=true&useMessageBodyForTemplate=true")
+ .to("mock:template");
}
};
}
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 41f6ca1f15c1..139a76288316 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
@@ -1717,6 +1717,18 @@ again. With `transacted=true` the SQL and MyBatis
consumers break out of the bat
`markRollbackOnlyLast()` reaches the consumer only when no `transacted()` EIP
has consumed the flag, so the
consumer's own transaction is the last one.
+=== camel-sql - sql and sql-stored with batch=true: the update count and a
single Map body
+
+With `batch=true` the `CamelSqlUpdateCount` (sql) and
`CamelSqlStoredUpdateCount` (sql-stored) headers no longer add
+the negative values a JDBC driver returns for an item whose update count it
does not know
+(`Statement.SUCCESS_NO_INFO`, which HSQLDB returns for stored procedure
calls). Such a batch reported a negative count
+before, for example `-4` for two items; it now reports the sum of the known
counts, which can be `0`.
+
+A single `Map` body is now one batch item, and so is a single `Map` in the
`CamelSqlParameters` header with
+`useMessageBodyForSql=true` (sql) or the `CamelSqlStoredParameters` header
with `useMessageBodyForTemplate=true`
+(sql-stored). Before, its entries were used as the batch items and the call
failed (sql: no value found for the named
+parameters, sql-stored: an `SQLException`).
+
=== camel-jta - an exchange marked rollback only last rolls back its
transaction
In a `transacted()` route that uses a JTA transaction manager,
`markRollbackOnlyLast()` now rolls back the
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SqlEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SqlEndpointBuilderFactory.java
index 848f3d681a93..7f51de105a80 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SqlEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SqlEndpointBuilderFactory.java
@@ -2700,7 +2700,10 @@ public interface SqlEndpointBuilderFactory {
/**
* The number of rows updated for update operations, returned as an
* Integer object. This header is not provided when using
- * outputType=StreamList.
+ * outputType=StreamList. With batch=true, the sum of the update counts
+ * of the batch items; an item the JDBC driver returns no count for
+ * (Statement.SUCCESS_NO_INFO) is not counted, so the value can be
lower
+ * than the number of rows updated, or 0.
*
* The option is a: {@code Integer} type.
*
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SqlStoredEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SqlStoredEndpointBuilderFactory.java
index 5378273bcbea..98ae45a7a8b5 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SqlStoredEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SqlStoredEndpointBuilderFactory.java
@@ -456,7 +456,10 @@ public interface SqlStoredEndpointBuilderFactory {
return "CamelSqlStoredParameters";
}
/**
- * The update count.
+ * The update count. With batch=true, the sum of the update counts of
+ * the batch items; an item the JDBC driver returns no count for
+ * (Statement.SUCCESS_NO_INFO) is not counted, so the value can be
lower
+ * than the number of rows updated, or 0.
*
* The option is a: {@code Integer} type.
*