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.
*