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 c09c01f957f9 CAMEL-26149: camel-duckdb - document the query option 
precedence, hint at the operation on a wrong statement kind, and accept a Path 
or bytes body for copy (#28919)
c09c01f957f9 is described below

commit c09c01f957f9de2afb21f8c2e32a6c626e912eb3
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:45:48 2026 +0100

    CAMEL-26149: camel-duckdb - document the query option precedence, hint at 
the operation on a wrong statement kind, and accept a Path or bytes body for 
copy (#28919)
    
    * CAMEL-26149: camel-duckdb - document the query option precedence, hin…
    
    …t at the operation on a wrong statement kind, and accept a Path or bytes 
body for copy
    
    The query option was documented for an empty body, but it replaces any body;
    the description now says so (the header wins over the option, the option 
over
    the body). A SELECT with operation=execute or DDL with operation=query 
failed
    with the plain driver error; the producer now runs the statement with
    execute() and fails with a DuckDbException that names the operation to use.
    Copy accepted only a File, a WrappedFile or a String; it now also takes a
    java.nio.file.Path and converts other bodies (such as byte[]) to a String.
    
    * CAMEL-26149: camel-duckdb - the copy error message lists bytes as a b…
    
    …ody type too
    
    The @BodyTypes for COPY lists bytes, so the message now says File, Path, 
String or bytes.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/catalog/components/duckdb.json    |  6 +-
 .../camel/catalog/docs/duckdb-component.adoc       | 10 ++-
 .../org/apache/camel/component/duckdb/duckdb.json  |  6 +-
 .../src/main/docs/duckdb-component.adoc            | 10 ++-
 .../camel/component/duckdb/DuckDbConstants.java    |  4 +-
 .../camel/component/duckdb/DuckDbEndpoint.java     |  8 +-
 .../camel/component/duckdb/DuckDbProducer.java     | 31 +++++--
 .../camel/component/duckdb/DuckDbProducerTest.java | 98 ++++++++++++++++++++++
 .../endpoint/dsl/DuckDbEndpointBuilderFactory.java |  7 +-
 9 files changed, 158 insertions(+), 22 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/duckdb.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/duckdb.json
index b018b667caae..a3915689bcac 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/duckdb.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/duckdb.json
@@ -34,13 +34,13 @@
     "CamelDuckDbOperation": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "Overrides the operation configured on the endpoint.", 
"constantName": "org.apache.camel.component.duckdb.DuckDbConstants#OPERATION" },
     "CamelDuckDbDatabasePath": { "index": 1, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "Overrides the database path configured on the 
endpoint.", "constantName": 
"org.apache.camel.component.duckdb.DuckDbConstants#DATABASE_PATH" },
     "CamelDuckDbTable": { "index": 2, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "Overrides the target table configured on the endpoint.", 
"constantName": "org.apache.camel.component.duckdb.DuckDbConstants#TABLE" },
-    "CamelDuckDbQuery": { "index": 3, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "Overrides the SQL for query or execute operations.", 
"constantName": "org.apache.camel.component.duckdb.DuckDbConstants#QUERY" },
+    "CamelDuckDbQuery": { "index": 3, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "Overrides the SQL for query or execute operations. Takes 
precedence over the query option and the message body.", "constantName": 
"org.apache.camel.component.duckdb.DuckDbConstants#QUERY" },
     "CamelDuckDbRowsWritten": { "index": 4, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "long", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The number of rows written or affected.", 
"constantName": 
"org.apache.camel.component.duckdb.DuckDbConstants#ROWS_WRITTEN" },
     "CamelDuckDbRowsRead": { "index": 5, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "long", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The number of rows returned by a query.", 
"constantName": "org.apache.camel.component.duckdb.DuckDbConstants#ROWS_READ" },
     "CamelDuckDbPingOk": { "index": 6, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "boolean", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The boolean result of a ping operation.", 
"constantName": "org.apache.camel.component.duckdb.DuckDbConstants#PING_OK" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], 
"dependsOn": [ "query" ], "byOption": [ { "option": "operation", "value": 
"INSERT", "carriers": [ "list" ] }, { "option": "operation", "value": "COPY", 
"carriers": [ "file", "text" ] }, { "option": "operation", "value": "PING", 
"carriers": [ "any" ] } ], "description": "The SQL statement as text, or bytes, 
a stream or a file of it; the query option or the CamelDuckDbQuery header takes 
its place. Insert takes a List of [...]
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], 
"dependsOn": [ "query" ], "byOption": [ { "option": "operation", "value": 
"INSERT", "carriers": [ "list" ] }, { "option": "operation", "value": "COPY", 
"carriers": [ "file", "text", "bytes" ] }, { "option": "operation", "value": 
"PING", "carriers": [ "any" ] } ], "description": "The SQL statement as text, 
or bytes, a stream or a file of it; the query option or the CamelDuckDbQuery 
header takes its place. Insert takes  [...]
     "producerOut": { "carriers": [ "value" ], "javaType": "java.lang.Long", 
"byOption": [ { "option": "operation", "value": "QUERY", "carriers": [ "list" 
], "javaType": "java.util.List<java.util.Map<String, Object>>" }, { "option": 
"operation", "value": "PING", "carriers": [ "value" ], "javaType": 
"java.lang.Boolean" }, { "option": "resultFormat", "value": "JSON", "carriers": 
[ "text", "value" ] } ], "description": "The count of rows written or affected 
as a Long, also in the CamelDuckDb [...]
   },
   "properties": {
@@ -49,7 +49,7 @@
     "format": { "index": 2, "kind": "parameter", "displayName": "Format", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "auto", "description": "File format for copy: 
csv, parquet, json or auto (inferred from the file extension)." },
     "jdbcUrl": { "index": 3, "kind": "parameter", "displayName": "Jdbc Url", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "Full JDBC URL override. When set, databasePath 
is not used to build the URL." },
     "operation": { "index": 4, "kind": "parameter", "displayName": 
"Operation", "group": "producer", "label": "", "required": false, "type": 
"enum", "javaType": "org.apache.camel.component.duckdb.DuckDbOperation", 
"enum": [ "EXECUTE", "QUERY", "INSERT", "COPY", "PING" ], "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": "EXECUTE", "description": 
"The operation to perform: execute, query, insert, copy or ping." },
-    "query": { "index": 5, "kind": "parameter", "displayName": "Query", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "Static SQL for query when the message body is 
empty." },
+    "query": { "index": 5, "kind": "parameter", "displayName": "Query", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "Static SQL for the execute and query 
operations. When set, it is used instead of the message body; the 
CamelDuckDbQuery header takes precedence over it." },
     "readOnly": { "index": 6, "kind": "parameter", "displayName": "Read Only", 
"group": "producer", "label": "", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": false, "description": "Open the embedded database file 
in read-only mode. Only applies to connections created by this endpoint from 
databasePath or jdbcUrl, and requires an existing database file." },
     "resultFormat": { "index": 7, "kind": "parameter", "displayName": "Result 
Format", "group": "producer", "label": "", "required": false, "type": "enum", 
"javaType": "org.apache.camel.component.duckdb.DuckDbResultFormat", "enum": [ 
"LIST_MAP", "JSON" ], "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": "LIST_MAP", "description": "How query results are returned: 
LIST_MAP (List of Map) or JSON array string." },
     "table": { "index": 8, "kind": "parameter", "displayName": "Table", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "Target table for insert and copy operations. 
Can be overridden per message with the CamelDuckDbTable header." },
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/duckdb-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/duckdb-component.adoc
index c70eaee36094..727bc281c342 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/duckdb-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/duckdb-component.adoc
@@ -53,12 +53,18 @@ include::partial$component-endpoint-headers.adoc[]
 
 The producer supports:
 
-* `execute` (default) — run DDL/DML from the message body (or `query` option / 
`CamelDuckDbQuery` header)
+* `execute` (default) — run DDL/DML that returns no rows
 * `query` — run a SELECT and return rows as `List<Map>` (default) or JSON 
(`resultFormat=JSON`)
 * `insert` — insert a `List` of `Map` rows into `table`
-* `copy` — bulk load from a `File` or path `String` using DuckDB `read_csv` / 
`read_parquet` / `read_json`
+* `copy` — bulk load from a `File`, a `Path` or a path as `String` (or bytes) 
using DuckDB `read_csv` / `read_parquet` / `read_json`
 * `ping` — `SELECT 1` connectivity check; sets `CamelDuckDbPingOk`
 
+The SQL for `execute` and `query` is taken from the `CamelDuckDbQuery` header, 
else from the `query` option, else
+from the message body. So when the `query` option is set, the message body is 
not used as SQL.
+
+A statement that returns rows fails with `execute`, and a statement that 
returns no rows (such as DDL) fails with
+`query`; the error says which operation to use instead. DuckDB has run the 
statement when the error is raised.
+
 The endpoint reuses one JDBC connection for its configured `databasePath` / 
`jdbcUrl` and serializes access for
 thread safety. Prefer a shared `DataSource` when you need concurrent 
producers. Set the
 `CamelDuckDbDatabasePath` header to open a short-lived connection to a 
different database file for a single
diff --git 
a/components/camel-duckdb/src/generated/resources/META-INF/org/apache/camel/component/duckdb/duckdb.json
 
b/components/camel-duckdb/src/generated/resources/META-INF/org/apache/camel/component/duckdb/duckdb.json
index b018b667caae..a3915689bcac 100644
--- 
a/components/camel-duckdb/src/generated/resources/META-INF/org/apache/camel/component/duckdb/duckdb.json
+++ 
b/components/camel-duckdb/src/generated/resources/META-INF/org/apache/camel/component/duckdb/duckdb.json
@@ -34,13 +34,13 @@
     "CamelDuckDbOperation": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "Overrides the operation configured on the endpoint.", 
"constantName": "org.apache.camel.component.duckdb.DuckDbConstants#OPERATION" },
     "CamelDuckDbDatabasePath": { "index": 1, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "Overrides the database path configured on the 
endpoint.", "constantName": 
"org.apache.camel.component.duckdb.DuckDbConstants#DATABASE_PATH" },
     "CamelDuckDbTable": { "index": 2, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "Overrides the target table configured on the endpoint.", 
"constantName": "org.apache.camel.component.duckdb.DuckDbConstants#TABLE" },
-    "CamelDuckDbQuery": { "index": 3, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "Overrides the SQL for query or execute operations.", 
"constantName": "org.apache.camel.component.duckdb.DuckDbConstants#QUERY" },
+    "CamelDuckDbQuery": { "index": 3, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "Overrides the SQL for query or execute operations. Takes 
precedence over the query option and the message body.", "constantName": 
"org.apache.camel.component.duckdb.DuckDbConstants#QUERY" },
     "CamelDuckDbRowsWritten": { "index": 4, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "long", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The number of rows written or affected.", 
"constantName": 
"org.apache.camel.component.duckdb.DuckDbConstants#ROWS_WRITTEN" },
     "CamelDuckDbRowsRead": { "index": 5, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "long", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The number of rows returned by a query.", 
"constantName": "org.apache.camel.component.duckdb.DuckDbConstants#ROWS_READ" },
     "CamelDuckDbPingOk": { "index": 6, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "boolean", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The boolean result of a ping operation.", 
"constantName": "org.apache.camel.component.duckdb.DuckDbConstants#PING_OK" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], 
"dependsOn": [ "query" ], "byOption": [ { "option": "operation", "value": 
"INSERT", "carriers": [ "list" ] }, { "option": "operation", "value": "COPY", 
"carriers": [ "file", "text" ] }, { "option": "operation", "value": "PING", 
"carriers": [ "any" ] } ], "description": "The SQL statement as text, or bytes, 
a stream or a file of it; the query option or the CamelDuckDbQuery header takes 
its place. Insert takes a List of [...]
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], 
"dependsOn": [ "query" ], "byOption": [ { "option": "operation", "value": 
"INSERT", "carriers": [ "list" ] }, { "option": "operation", "value": "COPY", 
"carriers": [ "file", "text", "bytes" ] }, { "option": "operation", "value": 
"PING", "carriers": [ "any" ] } ], "description": "The SQL statement as text, 
or bytes, a stream or a file of it; the query option or the CamelDuckDbQuery 
header takes its place. Insert takes  [...]
     "producerOut": { "carriers": [ "value" ], "javaType": "java.lang.Long", 
"byOption": [ { "option": "operation", "value": "QUERY", "carriers": [ "list" 
], "javaType": "java.util.List<java.util.Map<String, Object>>" }, { "option": 
"operation", "value": "PING", "carriers": [ "value" ], "javaType": 
"java.lang.Boolean" }, { "option": "resultFormat", "value": "JSON", "carriers": 
[ "text", "value" ] } ], "description": "The count of rows written or affected 
as a Long, also in the CamelDuckDb [...]
   },
   "properties": {
@@ -49,7 +49,7 @@
     "format": { "index": 2, "kind": "parameter", "displayName": "Format", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "auto", "description": "File format for copy: 
csv, parquet, json or auto (inferred from the file extension)." },
     "jdbcUrl": { "index": 3, "kind": "parameter", "displayName": "Jdbc Url", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "Full JDBC URL override. When set, databasePath 
is not used to build the URL." },
     "operation": { "index": 4, "kind": "parameter", "displayName": 
"Operation", "group": "producer", "label": "", "required": false, "type": 
"enum", "javaType": "org.apache.camel.component.duckdb.DuckDbOperation", 
"enum": [ "EXECUTE", "QUERY", "INSERT", "COPY", "PING" ], "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": "EXECUTE", "description": 
"The operation to perform: execute, query, insert, copy or ping." },
-    "query": { "index": 5, "kind": "parameter", "displayName": "Query", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "Static SQL for query when the message body is 
empty." },
+    "query": { "index": 5, "kind": "parameter", "displayName": "Query", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "Static SQL for the execute and query 
operations. When set, it is used instead of the message body; the 
CamelDuckDbQuery header takes precedence over it." },
     "readOnly": { "index": 6, "kind": "parameter", "displayName": "Read Only", 
"group": "producer", "label": "", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": false, "description": "Open the embedded database file 
in read-only mode. Only applies to connections created by this endpoint from 
databasePath or jdbcUrl, and requires an existing database file." },
     "resultFormat": { "index": 7, "kind": "parameter", "displayName": "Result 
Format", "group": "producer", "label": "", "required": false, "type": "enum", 
"javaType": "org.apache.camel.component.duckdb.DuckDbResultFormat", "enum": [ 
"LIST_MAP", "JSON" ], "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": "LIST_MAP", "description": "How query results are returned: 
LIST_MAP (List of Map) or JSON array string." },
     "table": { "index": 8, "kind": "parameter", "displayName": "Table", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "Target table for insert and copy operations. 
Can be overridden per message with the CamelDuckDbTable header." },
diff --git a/components/camel-duckdb/src/main/docs/duckdb-component.adoc 
b/components/camel-duckdb/src/main/docs/duckdb-component.adoc
index c70eaee36094..727bc281c342 100644
--- a/components/camel-duckdb/src/main/docs/duckdb-component.adoc
+++ b/components/camel-duckdb/src/main/docs/duckdb-component.adoc
@@ -53,12 +53,18 @@ include::partial$component-endpoint-headers.adoc[]
 
 The producer supports:
 
-* `execute` (default) — run DDL/DML from the message body (or `query` option / 
`CamelDuckDbQuery` header)
+* `execute` (default) — run DDL/DML that returns no rows
 * `query` — run a SELECT and return rows as `List<Map>` (default) or JSON 
(`resultFormat=JSON`)
 * `insert` — insert a `List` of `Map` rows into `table`
-* `copy` — bulk load from a `File` or path `String` using DuckDB `read_csv` / 
`read_parquet` / `read_json`
+* `copy` — bulk load from a `File`, a `Path` or a path as `String` (or bytes) 
using DuckDB `read_csv` / `read_parquet` / `read_json`
 * `ping` — `SELECT 1` connectivity check; sets `CamelDuckDbPingOk`
 
+The SQL for `execute` and `query` is taken from the `CamelDuckDbQuery` header, 
else from the `query` option, else
+from the message body. So when the `query` option is set, the message body is 
not used as SQL.
+
+A statement that returns rows fails with `execute`, and a statement that 
returns no rows (such as DDL) fails with
+`query`; the error says which operation to use instead. DuckDB has run the 
statement when the error is raised.
+
 The endpoint reuses one JDBC connection for its configured `databasePath` / 
`jdbcUrl` and serializes access for
 thread safety. Prefer a shared `DataSource` when you need concurrent 
producers. Set the
 `CamelDuckDbDatabasePath` header to open a short-lived connection to a 
different database file for a single
diff --git 
a/components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbConstants.java
 
b/components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbConstants.java
index d30d3ff152b0..ea3c5179a8b4 100644
--- 
a/components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbConstants.java
+++ 
b/components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbConstants.java
@@ -26,7 +26,9 @@ public final class DuckDbConstants {
     public static final String DATABASE_PATH = "CamelDuckDbDatabasePath";
     @Metadata(description = "Overrides the target table configured on the 
endpoint.", javaType = "String")
     public static final String TABLE = "CamelDuckDbTable";
-    @Metadata(description = "Overrides the SQL for query or execute 
operations.", javaType = "String")
+    @Metadata(description = "Overrides the SQL for query or execute 
operations. Takes precedence over the query option"
+                            + " and the message body.",
+              javaType = "String")
     public static final String QUERY = "CamelDuckDbQuery";
     @Metadata(description = "The number of rows written or affected.", 
javaType = "long")
     public static final String ROWS_WRITTEN = "CamelDuckDbRowsWritten";
diff --git 
a/components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbEndpoint.java
 
b/components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbEndpoint.java
index b41e0173c322..fd8a46099d9b 100644
--- 
a/components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbEndpoint.java
+++ 
b/components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbEndpoint.java
@@ -47,12 +47,13 @@ import org.apache.camel.util.ObjectHelper;
                                   byOption = {
                                           @BodyType.When(option = "operation", 
value = "INSERT", carriers = Carrier.LIST),
                                           @BodyType.When(option = "operation", 
value = "COPY",
-                                                         carriers = { 
Carrier.FILE, Carrier.TEXT }),
+                                                         carriers = { 
Carrier.FILE, Carrier.TEXT, Carrier.BYTES }),
                                           @BodyType.When(option = "operation", 
value = "PING", carriers = Carrier.ANY) },
                                   description = "The SQL statement as text, or 
bytes, a stream or a file of it; the query"
                                                 + " option or the 
CamelDuckDbQuery header takes its place. Insert takes a List"
                                                 + " of Maps of the column 
names and their values (a Map alone fails), copy a"
-                                                + " File or the path of the 
csv, parquet or json file to load"),
+                                                + " File, a Path or the path 
(as text or bytes) of the csv, parquet or json"
+                                                + " file to load"),
            producerOut = @BodyType(value = Carrier.VALUE, javaType = 
"java.lang.Long",
                                    byOption = {
                                            @BodyType.When(option = 
"operation", value = "QUERY", carriers = Carrier.LIST,
@@ -84,7 +85,8 @@ public class DuckDbEndpoint extends DefaultEndpoint {
     private String jdbcUrl;
     @UriParam(defaultValue = "EXECUTE", description = "The operation to 
perform: execute, query, insert, copy or ping.")
     private DuckDbOperation operation = DuckDbOperation.EXECUTE;
-    @UriParam(description = "Static SQL for query when the message body is 
empty.")
+    @UriParam(description = "Static SQL for the execute and query operations. 
When set, it is used instead of the"
+                            + " message body; the CamelDuckDbQuery header 
takes precedence over it.")
     private String query;
     @UriParam(defaultValue = "0",
               description = "Batch size for insert when the body is a List. 0 
inserts all rows in one JDBC batch.")
diff --git 
a/components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbProducer.java
 
b/components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbProducer.java
index 405cec5c5db5..72e5e58454f9 100644
--- 
a/components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbProducer.java
+++ 
b/components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbProducer.java
@@ -17,6 +17,7 @@
 package org.apache.camel.component.duckdb;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.sql.Connection;
 import java.sql.ResultSet;
 import java.sql.Statement;
@@ -67,7 +68,12 @@ public class DuckDbProducer extends DefaultProducer {
         }
         long updated;
         try (Statement statement = connection.createStatement()) {
-            updated = statement.executeUpdate(sql);
+            if (statement.execute(sql)) {
+                throw new DuckDbException(
+                        "The statement returns rows, which the execute 
operation does not read (DuckDB has run it)."
+                                          + " Use operation=query for a 
statement that returns rows.");
+            }
+            updated = statement.getUpdateCount();
         }
         setRowsWritten(exchange, updated);
         exchange.getMessage().setBody(updated);
@@ -78,9 +84,16 @@ public class DuckDbProducer extends DefaultProducer {
         if (ObjectHelper.isEmpty(sql)) {
             throw new DuckDbException("SQL is required for query (message 
body, query option or CamelDuckDbQuery header)");
         }
-        try (Statement statement = connection.createStatement();
-             ResultSet resultSet = statement.executeQuery(sql)) {
-            List<Map<String, Object>> rows = 
DuckDbResultSetMapper.toListMap(resultSet);
+        try (Statement statement = connection.createStatement()) {
+            if (!statement.execute(sql)) {
+                throw new DuckDbException(
+                        "The query operation expects a statement that returns 
rows, but this one returns none"
+                                          + " (DuckDB has run it). Use 
operation=execute for DDL or DML that returns no rows.");
+            }
+            List<Map<String, Object>> rows;
+            try (ResultSet resultSet = statement.getResultSet()) {
+                rows = DuckDbResultSetMapper.toListMap(resultSet);
+            }
             Message message = exchange.getMessage();
             message.setHeader(DuckDbConstants.ROWS_READ, (long) rows.size());
             if (endpoint.getResultFormat() == DuckDbResultFormat.JSON) {
@@ -170,10 +183,16 @@ public class DuckDbProducer extends DefaultProducer {
         if (body instanceof File file) {
             return file.getAbsolutePath();
         }
-        if (body instanceof String str && ObjectHelper.isNotEmpty(str)) {
+        if (body instanceof Path path) {
+            return path.toAbsolutePath().toString();
+        }
+        String str = body != null
+                ? 
exchange.getContext().getTypeConverter().tryConvertTo(String.class, exchange, 
body)
+                : null;
+        if (ObjectHelper.isNotEmpty(str)) {
             return str.trim();
         }
-        throw new DuckDbException("Copy operation expects a file path in the 
message body (File or String)");
+        throw new DuckDbException("Copy operation expects a file path in the 
message body (File, Path, String or bytes)");
     }
 
     private void setRowsWritten(Exchange exchange, long rows) {
diff --git 
a/components/camel-duckdb/src/test/java/org/apache/camel/component/duckdb/DuckDbProducerTest.java
 
b/components/camel-duckdb/src/test/java/org/apache/camel/component/duckdb/DuckDbProducerTest.java
index d56656b3bd19..7ed34cfc0e47 100644
--- 
a/components/camel-duckdb/src/test/java/org/apache/camel/component/duckdb/DuckDbProducerTest.java
+++ 
b/components/camel-duckdb/src/test/java/org/apache/camel/component/duckdb/DuckDbProducerTest.java
@@ -27,6 +27,7 @@ import java.sql.Statement;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.camel.CamelExecutionException;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
@@ -71,6 +72,103 @@ class DuckDbProducerTest extends CamelTestSupport {
         assertThat(rows).extracting(row -> 
row.get("name")).containsExactly("bob");
     }
 
+    @Test
+    void copyFromPathBody(@TempDir Path tempDir) throws Exception {
+        String uri = jdbcEndpoint(tempDir.resolve("test.db"));
+        template.sendBody(uri + "&operation=execute", "CREATE TABLE events (id 
INTEGER, name VARCHAR)");
+
+        Path csv = tempDir.resolve("data.csv");
+        Files.writeString(csv, "id,name\n3,carl\n");
+
+        Long written = template.requestBody(uri + 
"&operation=copy&table=events", csv, Long.class);
+        assertThat(written).isEqualTo(1L);
+
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> rows = template.requestBody(uri + 
"&operation=query", "SELECT name FROM events", List.class);
+        assertThat(rows).extracting(row -> 
row.get("name")).containsExactly("carl");
+    }
+
+    @Test
+    void copyFromBytesBody(@TempDir Path tempDir) throws Exception {
+        String uri = jdbcEndpoint(tempDir.resolve("test.db"));
+        template.sendBody(uri + "&operation=execute", "CREATE TABLE events (id 
INTEGER, name VARCHAR)");
+
+        Path csv = tempDir.resolve("data.csv");
+        Files.writeString(csv, "id,name\n4,dora\n");
+
+        byte[] body = 
csv.toAbsolutePath().toString().getBytes(StandardCharsets.UTF_8);
+        Long written = template.requestBody(uri + 
"&operation=copy&table=events", body, Long.class);
+        assertThat(written).isEqualTo(1L);
+
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> rows = template.requestBody(uri + 
"&operation=query", "SELECT name FROM events", List.class);
+        assertThat(rows).extracting(row -> 
row.get("name")).containsExactly("dora");
+    }
+
+    @Test
+    void copyWithoutPathFails(@TempDir Path tempDir) {
+        String uri = jdbcEndpoint(tempDir.resolve("test.db"));
+
+        assertThatThrownBy(() -> template.sendBody(uri + 
"&operation=copy&table=events", null))
+                .isInstanceOf(CamelExecutionException.class)
+                .cause()
+                .isInstanceOf(DuckDbException.class)
+                .hasMessageContaining("File, Path, String or bytes");
+    }
+
+    @Test
+    void queryOptionTakesPrecedenceOverBody(@TempDir Path tempDir) throws 
Exception {
+        String uri = jdbcEndpoint(tempDir.resolve("test.db"));
+        template.sendBody(uri + "&operation=execute", "CREATE TABLE events (id 
INTEGER, name VARCHAR)");
+        template.sendBody(uri + "&operation=execute", "INSERT INTO events 
VALUES (1, 'ada'), (2, 'bob')");
+
+        String queryUri = uri + "&operation=query&query=" + 
URLEncoder.encode("SELECT name FROM events WHERE id = 1",
+                StandardCharsets.UTF_8);
+
+        // the query option is used, and the body is not read as SQL
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> rows = template.requestBody(queryUri, 
"SELECT name FROM events WHERE id = 2", List.class);
+        assertThat(rows).extracting(row -> 
row.get("name")).containsExactly("ada");
+
+        // the header takes precedence over the query option
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> headerRows = 
template.requestBodyAndHeader(queryUri, null, DuckDbConstants.QUERY,
+                "SELECT name FROM events WHERE id = 2", List.class);
+        assertThat(headerRows).extracting(row -> 
row.get("name")).containsExactly("bob");
+    }
+
+    @Test
+    void executeReturnsUpdateCount(@TempDir Path tempDir) throws Exception {
+        String uri = jdbcEndpoint(tempDir.resolve("test.db"));
+        template.sendBody(uri + "&operation=execute", "CREATE TABLE events (id 
INTEGER, name VARCHAR)");
+
+        Long updated = template.requestBody(uri + "&operation=execute",
+                "INSERT INTO events VALUES (1, 'ada'), (2, 'bob')", 
Long.class);
+        assertThat(updated).isEqualTo(2L);
+    }
+
+    @Test
+    void executeWithSelectHintsAtQueryOperation(@TempDir Path tempDir) {
+        String uri = jdbcEndpoint(tempDir.resolve("test.db"));
+
+        assertThatThrownBy(() -> template.sendBody(uri + "&operation=execute", 
"SELECT 1"))
+                .isInstanceOf(CamelExecutionException.class)
+                .cause()
+                .isInstanceOf(DuckDbException.class)
+                .hasMessageContaining("operation=query");
+    }
+
+    @Test
+    void queryWithDdlHintsAtExecuteOperation(@TempDir Path tempDir) {
+        String uri = jdbcEndpoint(tempDir.resolve("test.db"));
+
+        assertThatThrownBy(() -> template.sendBody(uri + "&operation=query", 
"CREATE TABLE events (id INTEGER)"))
+                .isInstanceOf(CamelExecutionException.class)
+                .cause()
+                .isInstanceOf(DuckDbException.class)
+                .hasMessageContaining("operation=execute");
+    }
+
     @Test
     void insertQuotesReservedIdentifiers(@TempDir Path tempDir) throws 
Exception {
         String uri = jdbcEndpoint(tempDir.resolve("test.db"));
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DuckDbEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DuckDbEndpointBuilderFactory.java
index af1d5b494565..93a849b44cca 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DuckDbEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DuckDbEndpointBuilderFactory.java
@@ -141,7 +141,9 @@ public interface DuckDbEndpointBuilderFactory {
             return this;
         }
         /**
-         * Static SQL for query when the message body is empty.
+         * Static SQL for the execute and query operations. When set, it is 
used
+         * instead of the message body; the CamelDuckDbQuery header takes
+         * precedence over it.
          * 
          * The option is a: <code>java.lang.String</code> type.
          * 
@@ -405,7 +407,8 @@ public interface DuckDbEndpointBuilderFactory {
             return "CamelDuckDbTable";
         }
         /**
-         * Overrides the SQL for query or execute operations.
+         * Overrides the SQL for query or execute operations. Takes precedence
+         * over the query option and the message body.
          * 
          * The option is a: {@code String} type.
          * 

Reply via email to