gnodet commented on code in PR #25102:
URL: https://github.com/apache/camel/pull/25102#discussion_r3647499505


##########
components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbException.java:
##########
@@ -0,0 +1,28 @@
+/*
+ * 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.duckdb;
+
+public class DuckDbException extends Exception {

Review Comment:
   **Compilation error:** `DuckDbException` extends `Exception` (checked), but 
`buildCopySql()` and `resolveFormat()` in `DuckDbProducer` throw it without 
declaring `throws DuckDbException` in their method signatures. This will cause 
`javac` to reject the code.
   
   Suggested fix — change to `RuntimeException` (more consistent with typical 
Camel exception patterns):
   
   ```suggestion
   public class DuckDbException extends RuntimeException {
   ```



##########
components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbProducer.java:
##########
@@ -0,0 +1,235 @@
+/*
+ * 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.duckdb;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+import org.apache.camel.WrappedFile;
+import org.apache.camel.support.DefaultProducer;
+import org.apache.camel.util.ObjectHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DuckDbProducer extends DefaultProducer {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(DuckDbProducer.class);
+
+    private final DuckDbEndpoint endpoint;
+
+    public DuckDbProducer(DuckDbEndpoint endpoint) {
+        super(endpoint);
+        this.endpoint = endpoint;
+    }
+
+    @Override
+    public void process(Exchange exchange) throws Exception {
+        DuckDbOperation operation = resolveOperation(exchange);
+        try (ConnectionScope scope = ConnectionScope.open(endpoint)) {
+            Connection connection = scope.connection();
+            switch (operation) {
+                case EXECUTE -> doExecute(exchange, connection);
+                case QUERY -> doQuery(exchange, connection);
+                case INSERT -> doInsert(exchange, connection);
+                case COPY -> doCopy(exchange, connection);
+                case PING -> doPing(exchange, connection);
+                default -> throw new DuckDbException("Unsupported operation: " 
+ operation);
+            }
+        }
+    }
+
+    private void doExecute(Exchange exchange, Connection connection) throws 
Exception {
+        String sql = resolveSql(exchange);
+        if (ObjectHelper.isEmpty(sql)) {
+            throw new DuckDbException("SQL is required for execute (message 
body or query option)");
+        }
+        long updated;
+        try (Statement statement = connection.createStatement()) {
+            updated = statement.executeUpdate(sql);
+        }
+        setRowsWritten(exchange, updated);
+        exchange.getMessage().setBody(updated);
+    }
+
+    private void doQuery(Exchange exchange, Connection connection) throws 
Exception {
+        String sql = resolveSql(exchange);
+        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);
+            Message message = exchange.getMessage();
+            message.setHeader(DuckDbConstants.ROWS_READ, (long) rows.size());
+            if (endpoint.getResultFormat() == DuckDbResultFormat.JSON) {
+                message.setBody(DuckDbResultSetMapper.toJson(rows));
+            } else {
+                message.setBody(rows);
+            }
+        }
+    }
+
+    private void doInsert(Exchange exchange, Connection connection) throws 
Exception {
+        String table = resolveTable(exchange);
+        if (ObjectHelper.isEmpty(table)) {
+            throw new DuckDbException(
+                    "A table is required for insert. Provide 
databasePath/table, the table option, or the "
+                                          + DuckDbConstants.TABLE + " 
header.");
+        }
+        List<?> body = exchange.getIn().getBody(List.class);
+        if (body == null) {
+            throw new DuckDbException("Insert operation expects the message 
body to be a List of rows");
+        }
+        long written = DuckDbInsertSupport.insertMaps(connection, table, body, 
endpoint.getBatchSize());
+        LOG.debug("Inserted {} rows into {}", written, table);
+        setRowsWritten(exchange, written);
+    }
+
+    private void doCopy(Exchange exchange, Connection connection) throws 
Exception {
+        String table = resolveTable(exchange);
+        if (ObjectHelper.isEmpty(table)) {
+            throw new DuckDbException("A table is required for copy 
operations");
+        }
+        String path = resolveFilePath(exchange);
+        String format = resolveFormat(path);
+        String sql = buildCopySql(table, path, format);
+        long updated;
+        try (Statement statement = connection.createStatement()) {
+            updated = statement.executeUpdate(sql);
+        }
+        setRowsWritten(exchange, updated);
+    }
+
+    private void doPing(Exchange exchange, Connection connection) throws 
Exception {
+        boolean ok;
+        try (Statement statement = connection.createStatement();
+             ResultSet rs = statement.executeQuery("SELECT 1")) {
+            ok = rs.next() && rs.getInt(1) == 1;
+        }
+        Message message = exchange.getMessage();
+        message.setHeader(DuckDbConstants.PING_OK, ok);
+        message.setBody(ok);
+    }
+
+    private String buildCopySql(String table, String path, String format) {

Review Comment:
   This method throws checked `DuckDbException` in the `default` branch but the 
signature does not declare `throws`. This is part of the compilation error 
described on `DuckDbException.java` — if `DuckDbException` is changed to extend 
`RuntimeException`, this is resolved automatically.



##########
components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbInsertSupport.java:
##########
@@ -0,0 +1,84 @@
+/*
+ * 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.duckdb;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+final class DuckDbInsertSupport {
+
+    private DuckDbInsertSupport() {
+    }
+
+    static long insertMaps(Connection connection, String table, List<?> rows, 
int batchSize) throws Exception {
+        if (rows.isEmpty()) {
+            return 0;
+        }
+        if (!(rows.get(0) instanceof Map)) {
+            throw new DuckDbException("Insert body must be a List of Map 
entries when using operation=insert");
+        }
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> maps = (List<Map<String, Object>>) rows;
+        List<String> columns = new ArrayList<>(maps.get(0).keySet());
+        if (columns.isEmpty()) {
+            throw new DuckDbException("Insert row maps must contain at least 
one column");
+        }
+
+        String sql = buildInsertSql(table, columns);
+        long written = 0;
+        int effectiveBatch = batchSize > 0 ? batchSize : maps.size();
+        try (PreparedStatement ps = connection.prepareStatement(sql)) {
+            for (int i = 0; i < maps.size(); i++) {
+                Map<String, Object> row = maps.get(i);
+                for (int c = 0; c < columns.size(); c++) {
+                    ps.setObject(c + 1, row.get(columns.get(c)));
+                }
+                ps.addBatch();
+                if ((i + 1) % effectiveBatch == 0 || i + 1 == maps.size()) {
+                    int[] counts = ps.executeBatch();
+                    for (int count : counts) {
+                        if (count >= 0) {
+                            written += count;
+                        } else {
+                            written += 1;
+                        }
+                    }
+                }
+            }
+        }
+        return written;
+    }
+
+    private static String buildInsertSql(String table, List<String> columns) {
+        StringBuilder sql = new StringBuilder("INSERT INTO ");
+        sql.append(table).append(" (");

Review Comment:
   **Defense-in-depth:** Table and column names are interpolated directly into 
SQL without quoting. While route authors are trusted per Camel's security 
model, wrapping identifiers in double quotes provides defense against reserved 
words and special characters in identifiers:
   
   ```java
   sql.append('"').append(table).append('"').append(" (");
   ```
   
   Same applies to column names in this method and in 
`DuckDbProducer.buildCopySql()`.



##########
components/camel-duckdb/src/main/java/org/apache/camel/component/duckdb/DuckDbEndpoint.java:
##########
@@ -0,0 +1,214 @@
+/*
+ * 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.duckdb;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+
+import javax.sql.DataSource;
+
+import org.apache.camel.Category;
+import org.apache.camel.Consumer;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriEndpoint;
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriPath;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.util.ObjectHelper;
+
+/**
+ * Interact with <a href="https://duckdb.org/";>DuckDB</a>, the in-process 
analytical SQL database, for embedded
+ * analytics workloads.
+ */
+@UriEndpoint(firstVersion = "4.22.0", scheme = "duckdb", title = "DuckDB",
+             syntax = "duckdb:databasePath", category = { Category.DATABASE, 
Category.BIGDATA },
+             producerOnly = true, headersClass = DuckDbConstants.class)
+public class DuckDbEndpoint extends DefaultEndpoint {
+
+    private DataSource dataSource;
+    private Connection connection;
+    private Connection injectedConnection;
+    private boolean connectionOwned;
+
+    @UriPath(description = "Database path (:memory: or a file path). An 
optional /table segment sets the default table.")
+    private String databasePath = ":memory:";
+    @UriParam(description = "Target table for insert and copy operations. Can 
also be set in the path as databasePath/table"
+                            + " or overridden with the CamelDuckDbTable 
header.")
+    private String table;
+    @UriParam(description = "Full JDBC URL override. When set, databasePath is 
not used to build the URL.")
+    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.")
+    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.")
+    private int batchSize;
+    @UriParam(defaultValue = "auto",
+              description = "File format for copy: csv, parquet, json or auto 
(inferred from the file extension).")
+    private String format = "auto";
+    @UriParam(defaultValue = "false", description = "Open the embedded 
database file in read-only mode when supported.")
+    private boolean readOnly;

Review Comment:
   **Dead parameter:** The `readOnly` `@UriParam` is declared and documented, 
but is never referenced in `doStart()`, `resolveJdbcUrl()`, or anywhere in 
connection setup. Users setting `readOnly=true` would expect read-only mode but 
it has no effect.
   
   Either implement it (e.g., append `?access_mode=read_only` to the JDBC URL 
or pass a `Properties` object to `DriverManager.getConnection()`) or remove it 
entirely.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to