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 274c020823f2 chore: camel-jbang - Add SQL query dev-console, TUI tab, 
and MCP tools
274c020823f2 is described below

commit 274c020823f2397b01b2e75f7521586543b4a231
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jun 29 07:47:45 2026 +0200

    chore: camel-jbang - Add SQL query dev-console, TUI tab, and MCP tools
    
    Add sql-query dev-console for executing SQL queries and row updates
    against DataSource beans via JDBC. Includes:
    
    - SqlQueryDevConsole with support for SELECT, INSERT, UPDATE, DELETE
      and inline row editing with auto-generated UPDATE via PK detection
    - camel sql CLI command for terminal-based database queries
    - SQL Query TUI tab with multi-line input, query history (Ctrl+E),
      datasource switching, and tabular result display
    - tui_execute_sql, tui_update_row, and tui_set_input MCP tools for
      AI agent integration
    - Enriched tui_get_table MCP response with SQL input state and
      editability metadata
    - Support loading SQL from file via file: prefix
    - SQL example with 5s query period
    
    Closes #24281
    
    Co-Authored-By: Claude <[email protected]>
---
 .../apache/camel/catalog/dev-consoles.properties   |    1 +
 .../camel/catalog/dev-consoles/sql-query.json      |   15 +
 .../impl/console/SqlQueryDevConsoleConfigurer.java |   77 ++
 .../org/apache/camel/dev-console/sql-query.json    |   15 +
 ...rg.apache.camel.impl.console.SqlQueryDevConsole |    2 +
 .../org/apache/camel/dev-console/sql-query         |    2 +
 .../org/apache/camel/dev-consoles.properties       |    2 +-
 .../camel/impl/console/SqlQueryDevConsole.java     |  531 ++++++++++
 .../pages/jbang-commands/camel-jbang-cmd-sql.adoc  |   29 +
 .../ROOT/pages/jbang-commands/camel-jbang-cmd.adoc |    1 +
 .../camel/cli/connector/LocalCliConnector.java     |   54 +
 .../META-INF/camel-jbang-commands-metadata.json    |    2 +-
 .../dsl/jbang/core/commands/CamelJBangMain.java    |    1 +
 .../core/commands/action/CamelSqlQueryAction.java  |  174 ++++
 .../camel/dsl/jbang/core/common/RuntimeHelper.java |   44 +-
 .../src/main/resources/examples/sql/sql.camel.yaml |    2 +-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  |   82 +-
 .../dsl/jbang/core/commands/tui/ClasspathTab.java  |    8 +
 .../camel/dsl/jbang/core/commands/tui/HttpTab.java |   17 +
 .../dsl/jbang/core/commands/tui/InputHistory.java  |  153 +++
 .../dsl/jbang/core/commands/tui/MonitorTab.java    |    4 +
 .../dsl/jbang/core/commands/tui/SpansTab.java      |    8 +
 .../dsl/jbang/core/commands/tui/SqlQueryTab.java   | 1057 ++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/TuiMcpServer.java  |   94 ++
 24 files changed, 2355 insertions(+), 20 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
index 536dbe0e6b98..217aa1876741 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
@@ -56,6 +56,7 @@ service
 sftp
 simple-language
 source
+sql-query
 startup-recorder
 stub
 system-properties
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/sql-query.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/sql-query.json
new file mode 100644
index 000000000000..1b98f12a4690
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/sql-query.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "sql-query",
+    "title": "SQL Query",
+    "description": "Execute SQL queries on DataSource beans",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.SqlQueryDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.21.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/core/camel-console/src/generated/java/org/apache/camel/impl/console/SqlQueryDevConsoleConfigurer.java
 
b/core/camel-console/src/generated/java/org/apache/camel/impl/console/SqlQueryDevConsoleConfigurer.java
new file mode 100644
index 000000000000..61bc950601a3
--- /dev/null
+++ 
b/core/camel-console/src/generated/java/org/apache/camel/impl/console/SqlQueryDevConsoleConfigurer.java
@@ -0,0 +1,77 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.impl.console;
+
+import javax.annotation.processing.Generated;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.impl.console.SqlQueryDevConsole;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class SqlQueryDevConsoleConfigurer extends 
org.apache.camel.support.component.PropertyConfigurerSupport implements 
GeneratedPropertyConfigurer, ExtendedPropertyConfigurerGetter {
+
+    private static final Map<String, Object> ALL_OPTIONS;
+    static {
+        Map<String, Object> map = new CaseInsensitiveMap();
+        map.put("CamelContext", org.apache.camel.CamelContext.class);
+        map.put("DefaultMaxRows", int.class);
+        map.put("DefaultQueryTimeout", int.class);
+        ALL_OPTIONS = map;
+    }
+
+    @Override
+    public boolean configure(CamelContext camelContext, Object obj, String 
name, Object value, boolean ignoreCase) {
+        org.apache.camel.impl.console.SqlQueryDevConsole target = 
(org.apache.camel.impl.console.SqlQueryDevConsole) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "camelcontext":
+        case "camelContext": target.setCamelContext(property(camelContext, 
org.apache.camel.CamelContext.class, value)); return true;
+        case "defaultmaxrows":
+        case "defaultMaxRows": target.setDefaultMaxRows(property(camelContext, 
int.class, value)); return true;
+        case "defaultquerytimeout":
+        case "defaultQueryTimeout": 
target.setDefaultQueryTimeout(property(camelContext, int.class, value)); return 
true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public Map<String, Object> getAllOptions(Object target) {
+        return ALL_OPTIONS;
+    }
+
+    @Override
+    public Class<?> getOptionType(String name, boolean ignoreCase) {
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "camelcontext":
+        case "camelContext": return org.apache.camel.CamelContext.class;
+        case "defaultmaxrows":
+        case "defaultMaxRows": return int.class;
+        case "defaultquerytimeout":
+        case "defaultQueryTimeout": return int.class;
+        default: return null;
+        }
+    }
+
+    @Override
+    public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+        org.apache.camel.impl.console.SqlQueryDevConsole target = 
(org.apache.camel.impl.console.SqlQueryDevConsole) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "camelcontext":
+        case "camelContext": return target.getCamelContext();
+        case "defaultmaxrows":
+        case "defaultMaxRows": return target.getDefaultMaxRows();
+        case "defaultquerytimeout":
+        case "defaultQueryTimeout": return target.getDefaultQueryTimeout();
+        default: return null;
+        }
+    }
+}
+
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/sql-query.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/sql-query.json
new file mode 100644
index 000000000000..1b98f12a4690
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/sql-query.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "sql-query",
+    "title": "SQL Query",
+    "description": "Execute SQL queries on DataSource beans",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.SqlQueryDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.21.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.SqlQueryDevConsole
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.SqlQueryDevConsole
new file mode 100644
index 000000000000..cf682ab4c486
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.SqlQueryDevConsole
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.SqlQueryDevConsoleConfigurer
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/sql-query
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/sql-query
new file mode 100644
index 000000000000..9ddcee3835ca
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/sql-query
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.SqlQueryDevConsole
diff --git 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
index e25def6b5f32..40b26b866b52 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
@@ -1,5 +1,5 @@
 # Generated by camel build tools - do NOT edit this file!
-dev-consoles=bean blocked browse circuit-breaker consumer context datasource 
debug endpoint errors eval-language event gc health inflight internal-tasks 
java-security jvm log memory message-history processor producer properties 
receive reload rest rest-spec route route-controller route-dump route-group 
route-structure route-topology send service simple-language source 
startup-recorder system-properties thread top trace transformers 
type-converters variables
+dev-consoles=bean blocked browse circuit-breaker consumer context datasource 
debug endpoint errors eval-language event gc health inflight internal-tasks 
java-security jvm log memory message-history processor producer properties 
receive reload rest rest-spec route route-controller route-dump route-group 
route-structure route-topology send service simple-language source sql-query 
startup-recorder system-properties thread top trace transformers 
type-converters variables
 groupId=org.apache.camel
 artifactId=camel-console
 version=4.21.0-SNAPSHOT
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlQueryDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlQueryDevConsole.java
new file mode 100644
index 000000000000..4bd24bba64d2
--- /dev/null
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SqlQueryDevConsole.java
@@ -0,0 +1,531 @@
+/*
+ * 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.impl.console;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.sql.DataSource;
+
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+@DevConsole(name = "sql-query", displayName = "SQL Query", description = 
"Execute SQL queries on DataSource beans")
+@Configurer(extended = true)
+public class SqlQueryDevConsole extends AbstractDevConsole {
+
+    /**
+     * The SQL query to execute
+     */
+    public static final String SQL = "sql";
+
+    /**
+     * Name of the DataSource bean in the registry (auto-detected if only one 
exists)
+     */
+    public static final String DATASOURCE = "datasource";
+
+    /**
+     * Maximum number of rows to return
+     */
+    public static final String MAX_ROWS = "maxRows";
+
+    /**
+     * Query timeout in seconds
+     */
+    public static final String QUERY_TIMEOUT = "queryTimeout";
+
+    /**
+     * Action type: "query" (default) or "update-row"
+     */
+    public static final String ACTION_TYPE = "actionType";
+
+    /**
+     * Table name for update-row action
+     */
+    public static final String TABLE = "table";
+
+    /**
+     * Primary key column-value pairs as JSON string for update-row action
+     */
+    public static final String PRIMARY_KEY_VALUES = "primaryKeyValues";
+
+    /**
+     * Changed column-value pairs as JSON string for update-row action
+     */
+    public static final String COLUMN_VALUES = "columnValues";
+
+    @Metadata(defaultValue = "100",
+              description = "Maximum number of rows to return from a query")
+    private int defaultMaxRows = 100;
+
+    @Metadata(defaultValue = "30",
+              description = "Default query timeout in seconds")
+    private int defaultQueryTimeout = 30;
+
+    public SqlQueryDevConsole() {
+        super("camel", "sql-query", "SQL Query", "Execute SQL queries on 
DataSource beans");
+    }
+
+    public int getDefaultMaxRows() {
+        return defaultMaxRows;
+    }
+
+    public void setDefaultMaxRows(int defaultMaxRows) {
+        this.defaultMaxRows = defaultMaxRows;
+    }
+
+    public int getDefaultQueryTimeout() {
+        return defaultQueryTimeout;
+    }
+
+    public void setDefaultQueryTimeout(int defaultQueryTimeout) {
+        this.defaultQueryTimeout = defaultQueryTimeout;
+    }
+
+    @Override
+    protected String doCallText(Map<String, Object> options) {
+        StringBuilder sb = new StringBuilder();
+
+        String sql = (String) options.get(SQL);
+        if (sql == null || sql.isBlank()) {
+            sb.append("No SQL query provided\n");
+            return sb.toString();
+        }
+
+        String dsName = (String) options.get(DATASOURCE);
+        int maxRows = parseIntOption(options, MAX_ROWS, defaultMaxRows);
+        int queryTimeout = parseIntOption(options, QUERY_TIMEOUT, 
defaultQueryTimeout);
+
+        DataSource ds;
+        String resolvedName;
+        if (dsName != null && !dsName.isBlank()) {
+            ds = getCamelContext().getRegistry().lookupByNameAndType(dsName, 
DataSource.class);
+            resolvedName = dsName;
+            if (ds == null) {
+                sb.append(String.format("DataSource '%s' not found in 
registry%n", dsName));
+                return sb.toString();
+            }
+        } else {
+            Map<String, DataSource> all = 
getCamelContext().getRegistry().findByTypeWithName(DataSource.class);
+            if (all.isEmpty()) {
+                sb.append("No DataSource found in registry\n");
+                return sb.toString();
+            }
+            if (all.size() > 1) {
+                sb.append(String.format("Multiple DataSources found: %s. 
Specify one with --datasource%n",
+                        String.join(", ", all.keySet())));
+                return sb.toString();
+            }
+            Map.Entry<String, DataSource> single = 
all.entrySet().iterator().next();
+            ds = single.getValue();
+            resolvedName = single.getKey();
+        }
+
+        StopWatch watch = new StopWatch();
+        try (Connection conn = ds.getConnection();
+             Statement stmt = conn.createStatement()) {
+
+            stmt.setMaxRows(maxRows);
+            stmt.setQueryTimeout(queryTimeout);
+
+            boolean hasResultSet = stmt.execute(sql);
+            long elapsed = watch.taken();
+
+            if (hasResultSet) {
+                try (ResultSet rs = stmt.getResultSet()) {
+                    ResultSetMetaData meta = rs.getMetaData();
+                    int colCount = meta.getColumnCount();
+
+                    sb.append(String.format("DataSource: %s%n", resolvedName));
+                    sb.append(String.format("Elapsed: %s%n%n", 
TimeUtils.printDuration(elapsed)));
+
+                    // column headers
+                    for (int i = 1; i <= colCount; i++) {
+                        if (i > 1) {
+                            sb.append(" | ");
+                        }
+                        sb.append(meta.getColumnLabel(i));
+                    }
+                    sb.append("\n");
+
+                    int rowCount = 0;
+                    while (rs.next()) {
+                        for (int i = 1; i <= colCount; i++) {
+                            if (i > 1) {
+                                sb.append(" | ");
+                            }
+                            Object val = rs.getObject(i);
+                            sb.append(val != null ? String.valueOf(val) : 
"null");
+                        }
+                        sb.append("\n");
+                        rowCount++;
+                    }
+                    sb.append(String.format("%n%d row(s)%n", rowCount));
+                    if (rowCount >= maxRows) {
+                        sb.append("(truncated)\n");
+                    }
+                }
+            } else {
+                int updateCount = stmt.getUpdateCount();
+                sb.append(String.format("DataSource: %s%n", resolvedName));
+                sb.append(String.format("Update count: %d%n", updateCount));
+                sb.append(String.format("Elapsed: %s%n", 
TimeUtils.printDuration(elapsed)));
+            }
+        } catch (Exception e) {
+            long elapsed = watch.taken();
+            sb.append(String.format("Error: %s%n", e.getMessage()));
+            sb.append(String.format("Elapsed: %s%n", 
TimeUtils.printDuration(elapsed)));
+        }
+
+        return sb.toString();
+    }
+
+    @Override
+    protected JsonObject doCallJson(Map<String, Object> options) {
+        String actionType = (String) options.get(ACTION_TYPE);
+        if ("update-row".equals(actionType)) {
+            return doUpdateRow(options);
+        }
+        return doQuery(options);
+    }
+
+    private JsonObject doQuery(Map<String, Object> options) {
+        JsonObject root = new JsonObject();
+
+        String sql = (String) options.get(SQL);
+        if (sql == null || sql.isBlank()) {
+            root.put("status", "error");
+            root.put("message", "No SQL query provided");
+            return root;
+        }
+
+        String dsName = (String) options.get(DATASOURCE);
+        int maxRows = parseIntOption(options, MAX_ROWS, defaultMaxRows);
+        int queryTimeout = parseIntOption(options, QUERY_TIMEOUT, 
defaultQueryTimeout);
+
+        DataSource ds = resolveDataSource(dsName, root);
+        if (ds == null) {
+            return root;
+        }
+        String resolvedName = root.getString("datasource");
+
+        StopWatch watch = new StopWatch();
+        try (Connection conn = ds.getConnection();
+             Statement stmt = conn.createStatement()) {
+
+            stmt.setMaxRows(maxRows);
+            stmt.setQueryTimeout(queryTimeout);
+
+            boolean hasResultSet = stmt.execute(sql);
+            long elapsed = watch.taken();
+
+            root.put("status", "success");
+            root.put("elapsed", elapsed);
+
+            if (hasResultSet) {
+                // read all data and metadata from ResultSet first, then close 
it
+                // before any DatabaseMetaData calls (some drivers only 
support one
+                // open ResultSet per connection)
+                String singleTable = null;
+                String catalog = null;
+                String schema = null;
+                JsonArray columns = new JsonArray();
+                JsonArray rows = new JsonArray();
+                int rowCount = 0;
+                String[] colLabels;
+
+                try (ResultSet rs = stmt.getResultSet()) {
+                    ResultSetMetaData meta = rs.getMetaData();
+                    int colCount = meta.getColumnCount();
+                    colLabels = new String[colCount];
+
+                    // detect single-table query for editability
+                    boolean allSameTable = true;
+                    for (int i = 1; i <= colCount; i++) {
+                        colLabels[i - 1] = meta.getColumnLabel(i);
+                        String tn = meta.getTableName(i);
+                        if (tn == null || tn.isEmpty()) {
+                            allSameTable = false;
+                        } else if (singleTable == null) {
+                            singleTable = tn;
+                            catalog = meta.getCatalogName(i);
+                            schema = meta.getSchemaName(i);
+                        } else if (!singleTable.equalsIgnoreCase(tn)) {
+                            allSameTable = false;
+                        }
+                    }
+                    if (!allSameTable) {
+                        singleTable = null;
+                    }
+
+                    for (int i = 1; i <= colCount; i++) {
+                        JsonObject col = new JsonObject();
+                        col.put("name", meta.getColumnLabel(i));
+                        col.put("type", meta.getColumnTypeName(i));
+                        columns.add(col);
+                    }
+
+                    while (rs.next()) {
+                        JsonObject row = new JsonObject();
+                        for (int i = 1; i <= colCount; i++) {
+                            Object val = rs.getObject(i);
+                            if (val == null) {
+                                row.put(colLabels[i - 1], null);
+                            } else if (val instanceof Number n) {
+                                row.put(colLabels[i - 1], n);
+                            } else if (val instanceof Boolean b) {
+                                row.put(colLabels[i - 1], b);
+                            } else {
+                                row.put(colLabels[i - 1], String.valueOf(val));
+                            }
+                        }
+                        rows.add(row);
+                        rowCount++;
+                    }
+                }
+
+                // ResultSet is now closed — safe to query DatabaseMetaData
+                Set<String> pkColumns = new LinkedHashSet<>();
+                if (singleTable != null) {
+                    try {
+                        DatabaseMetaData dbMeta = conn.getMetaData();
+                        try (ResultSet pkRs = dbMeta.getPrimaryKeys(
+                                catalog != null && !catalog.isEmpty() ? 
catalog : null,
+                                schema != null && !schema.isEmpty() ? schema : 
null,
+                                singleTable)) {
+                            while (pkRs.next()) {
+                                pkColumns.add(pkRs.getString("COLUMN_NAME"));
+                            }
+                        }
+                    } catch (Exception e) {
+                        // PK lookup failed — not editable
+                    }
+                }
+
+                // annotate columns with PK info
+                if (singleTable != null && !pkColumns.isEmpty()) {
+                    for (int i = 0; i < columns.size(); i++) {
+                        JsonObject col = (JsonObject) columns.get(i);
+                        col.put("primaryKey", 
pkColumns.contains(col.getString("name")));
+                    }
+                    root.put("tableName", singleTable);
+                    JsonArray pkArr = new JsonArray();
+                    pkColumns.forEach(pkArr::add);
+                    root.put("primaryKeys", pkArr);
+                    root.put("editable", true);
+                }
+
+                root.put("columns", columns);
+                root.put("rows", rows);
+                root.put("rowCount", rowCount);
+                root.put("truncated", rowCount >= maxRows);
+            } else {
+                int updateCount = stmt.getUpdateCount();
+                root.put("updateCount", updateCount);
+            }
+        } catch (Exception e) {
+            long elapsed = watch.taken();
+            root.put("status", "error");
+            root.put("elapsed", elapsed);
+            root.put("message", e.getMessage());
+        }
+
+        return root;
+    }
+
+    private JsonObject doUpdateRow(Map<String, Object> options) {
+        JsonObject root = new JsonObject();
+
+        String tableName = (String) options.get(TABLE);
+        if (tableName == null || tableName.isBlank()) {
+            root.put("status", "error");
+            root.put("message", "No table name provided");
+            return root;
+        }
+
+        String pkJson = (String) options.get(PRIMARY_KEY_VALUES);
+        String colJson = (String) options.get(COLUMN_VALUES);
+        if (pkJson == null || colJson == null) {
+            root.put("status", "error");
+            root.put("message", "Missing primaryKeyValues or columnValues");
+            return root;
+        }
+
+        JsonObject pkValues;
+        JsonObject colValues;
+        try {
+            pkValues = (JsonObject) Jsoner.deserialize(pkJson);
+            colValues = (JsonObject) Jsoner.deserialize(colJson);
+        } catch (Exception e) {
+            root.put("status", "error");
+            root.put("message", "Invalid JSON: " + e.getMessage());
+            return root;
+        }
+
+        if (colValues.isEmpty()) {
+            root.put("status", "error");
+            root.put("message", "No columns to update");
+            return root;
+        }
+
+        String dsName = (String) options.get(DATASOURCE);
+        DataSource ds = resolveDataSource(dsName, root);
+        if (ds == null) {
+            return root;
+        }
+
+        // build UPDATE table SET col1=?, col2=? WHERE pk1=? AND pk2=?
+        List<String> setCols = new ArrayList<>(colValues.keySet());
+        List<String> pkCols = new ArrayList<>(pkValues.keySet());
+
+        StringBuilder sb = new StringBuilder("UPDATE ");
+        sb.append(tableName).append(" SET ");
+        for (int i = 0; i < setCols.size(); i++) {
+            if (i > 0) {
+                sb.append(", ");
+            }
+            sb.append(setCols.get(i)).append(" = ?");
+        }
+        sb.append(" WHERE ");
+        for (int i = 0; i < pkCols.size(); i++) {
+            if (i > 0) {
+                sb.append(" AND ");
+            }
+            sb.append(pkCols.get(i)).append(" = ?");
+        }
+
+        StopWatch watch = new StopWatch();
+        try (Connection conn = ds.getConnection();
+             PreparedStatement ps = conn.prepareStatement(sb.toString())) {
+
+            int idx = 1;
+            for (String col : setCols) {
+                setParameter(ps, idx++, colValues.get(col));
+            }
+            for (String col : pkCols) {
+                setParameter(ps, idx++, pkValues.get(col));
+            }
+
+            int updateCount = ps.executeUpdate();
+            long elapsed = watch.taken();
+            root.put("status", "success");
+            root.put("elapsed", elapsed);
+            root.put("updateCount", updateCount);
+        } catch (Exception e) {
+            long elapsed = watch.taken();
+            root.put("status", "error");
+            root.put("elapsed", elapsed);
+            root.put("message", e.getMessage());
+        }
+
+        return root;
+    }
+
+    private DataSource resolveDataSource(String dsName, JsonObject root) {
+        if (dsName != null && !dsName.isBlank()) {
+            DataSource ds = 
getCamelContext().getRegistry().lookupByNameAndType(dsName, DataSource.class);
+            if (ds == null) {
+                root.put("status", "error");
+                root.put("message", String.format("DataSource '%s' not found 
in registry", dsName));
+                return null;
+            }
+            root.put("datasource", dsName);
+            return ds;
+        }
+
+        Map<String, DataSource> all = 
getCamelContext().getRegistry().findByTypeWithName(DataSource.class);
+        if (all.isEmpty()) {
+            root.put("status", "error");
+            root.put("message", "No DataSource found in registry");
+            return null;
+        }
+        if (all.size() > 1) {
+            root.put("status", "error");
+            root.put("message", "Multiple DataSources found, specify one: " + 
String.join(", ", all.keySet()));
+            JsonArray names = new JsonArray();
+            all.keySet().forEach(names::add);
+            root.put("availableDataSources", names);
+            return null;
+        }
+        Map.Entry<String, DataSource> single = 
all.entrySet().iterator().next();
+        root.put("datasource", single.getKey());
+        return single.getValue();
+    }
+
+    private static void setParameter(PreparedStatement ps, int index, Object 
value) throws Exception {
+        if (value == null) {
+            ps.setNull(index, java.sql.Types.NULL);
+        } else if (value instanceof Number n) {
+            if (value instanceof Integer || value instanceof Long) {
+                ps.setLong(index, n.longValue());
+            } else {
+                ps.setDouble(index, n.doubleValue());
+            }
+        } else if (value instanceof Boolean b) {
+            ps.setBoolean(index, b);
+        } else {
+            String s = String.valueOf(value);
+            if ("null".equalsIgnoreCase(s)) {
+                ps.setNull(index, java.sql.Types.NULL);
+            } else if ("true".equalsIgnoreCase(s) || 
"false".equalsIgnoreCase(s)) {
+                ps.setBoolean(index, Boolean.parseBoolean(s));
+            } else {
+                // try numeric types so the JDBC driver gets the right type
+                try {
+                    ps.setLong(index, Long.parseLong(s));
+                } catch (NumberFormatException e1) {
+                    try {
+                        ps.setDouble(index, Double.parseDouble(s));
+                    } catch (NumberFormatException e2) {
+                        ps.setString(index, s);
+                    }
+                }
+            }
+        }
+    }
+
+    private static int parseIntOption(Map<String, Object> options, String key, 
int defaultValue) {
+        Object val = options.get(key);
+        if (val instanceof Number n) {
+            return n.intValue();
+        }
+        if (val instanceof String s && !s.isBlank()) {
+            try {
+                return Integer.parseInt(s);
+            } catch (NumberFormatException e) {
+                // ignore
+            }
+        }
+        return defaultValue;
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-sql.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-sql.adoc
new file mode 100644
index 000000000000..39ffafc1c416
--- /dev/null
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-sql.adoc
@@ -0,0 +1,29 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel cmd sql
+
+Execute SQL query on a DataSource
+
+
+== Usage
+
+[source,bash]
+----
+camel cmd sql [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--datasource` | Name of the DataSource bean (auto-detected if only one 
exists) |  | String
+| `--max-rows` | Maximum number of rows to return | 100 | int
+| `--query,--sql` | The SQL query to execute, or file:<path> to load from a 
file |  | String
+| `--query-timeout` | Query timeout in seconds | 30 | int
+| `-h,--help` | Display the help and sub-commands |  | boolean
+|===
+
+
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
index b3e799143a74..4e261d317b2d 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc
@@ -34,6 +34,7 @@ camel cmd [options]
 | xref:jbang-commands/camel-jbang-cmd-route-topology.adoc[route-topology] | 
Display inter-route topology connections
 | xref:jbang-commands/camel-jbang-cmd-send.adoc[send] | Send messages to 
endpoints
 | xref:jbang-commands/camel-jbang-cmd-span.adoc[span] | Display OpenTelemetry 
spans from running Camel integrations
+| xref:jbang-commands/camel-jbang-cmd-sql.adoc[sql] | Execute SQL query on a 
DataSource
 | xref:jbang-commands/camel-jbang-cmd-start-group.adoc[start-group] | Start 
Camel route groups
 | xref:jbang-commands/camel-jbang-cmd-start-route.adoc[start-route] | Start 
Camel routes
 | xref:jbang-commands/camel-jbang-cmd-stop-group.adoc[stop-group] | Stop Camel 
route groups
diff --git 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index 21688dde8ca6..fe943b169bb7 100644
--- 
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ 
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -371,6 +371,10 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
                 doActionReceiveTask(root);
             } else if ("readme".equals(action)) {
                 doActionReadmeTask(root);
+            } else if ("sql-query".equals(action)) {
+                doActionSqlQueryTask(root);
+            } else if ("sql-update-row".equals(action)) {
+                doActionSqlUpdateRowTask(root);
             } else if ("cli-debug".equals(action)) {
                 doActionCliDebug(root);
             }
@@ -1012,6 +1016,56 @@ public class LocalCliConnector extends ServiceSupport 
implements CliConnector, C
         }
     }
 
+    private void doActionSqlQueryTask(JsonObject root) throws Exception {
+        DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+                .resolveById("sql-query");
+        if (dc != null) {
+            String sql = root.getStringOrDefault("sql", "");
+            if (sql.startsWith("file:")) {
+                File f = new File(sql.substring(5));
+                if (f.exists() && f.isFile()) {
+                    sql = Files.readString(f.toPath()).trim();
+                }
+            }
+            String datasource = root.getString("datasource");
+            int maxRows = root.getIntegerOrDefault("maxRows", 100);
+            int queryTimeout = root.getIntegerOrDefault("queryTimeout", 30);
+            Map<String, Object> args = new HashMap<>();
+            args.put("sql", sql);
+            if (datasource != null) {
+                args.put("datasource", datasource);
+            }
+            args.put("maxRows", String.valueOf(maxRows));
+            args.put("queryTimeout", String.valueOf(queryTimeout));
+            JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON, 
args);
+            LOG.trace("Updating output file: {}", outputFile);
+            IOHelper.writeText(json.toJson(), outputFile);
+        } else {
+            IOHelper.writeText("{}", outputFile);
+        }
+    }
+
+    private void doActionSqlUpdateRowTask(JsonObject root) throws Exception {
+        DevConsole dc = 
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+                .resolveById("sql-query");
+        if (dc != null) {
+            Map<String, Object> args = new HashMap<>();
+            args.put("actionType", "update-row");
+            args.put("table", root.getString("table"));
+            String datasource = root.getString("datasource");
+            if (datasource != null) {
+                args.put("datasource", datasource);
+            }
+            args.put("primaryKeyValues", root.getString("primaryKeyValues"));
+            args.put("columnValues", root.getString("columnValues"));
+            JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON, 
args);
+            LOG.trace("Updating output file: {}", outputFile);
+            IOHelper.writeText(json.toJson(), outputFile);
+        } else {
+            IOHelper.writeText("{}", outputFile);
+        }
+    }
+
     private void doActionLoadTask(JsonObject root) throws Exception {
         List<String> files = root.getCollection("source");
         boolean restart = root.getBooleanOrDefault("restart", false);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 19c7b6505642..6ee1954980ad 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -3,7 +3,7 @@
     { "name": "ask", "fullName": "ask", "description": "Ask a question about a 
running Camel application using AI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": 
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", 
"type": "string" }, { "names": "--api-type", "description": "API type: 
'ollama', 'openai', or 'anthropic'", "javaType": "LlmClient.ApiType", "type" 
[...]
     { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind 
source and sink Kamelets as a new Camel integration", "deprecated": true, 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": 
[ { "names": "--error-handler", "description": "Add error handler 
(none|log|sink:<endpoint>). Sink endpoints are expected in the format 
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet 
name.", "javaType": "java.lang.String", "type": "stri [...]
     { "name": "catalog", "fullName": "catalog", "description": "List artifacts 
from Camel Catalog", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"component", "fullName": "catalog component", "description": "List components 
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
-    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
+    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate 
completion script for bash\/zsh", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user 
configuration values", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "config get", "description": "Display user configuration value", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
     { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To  [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 5113540c571f..da28f9ad87bb 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -121,6 +121,7 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("route-structure", new CommandLine(new 
CamelRouteStructureAction(this)))
                         .addSubcommand("route-topology", new CommandLine(new 
CamelRouteTopologyAction(this)))
                         .addSubcommand("send", new CommandLine(new 
CamelSendAction(this)))
+                        .addSubcommand("sql", new CommandLine(new 
CamelSqlQueryAction(this)))
                         .addSubcommand("span", new CommandLine(new 
CamelSpanAction(this)))
                         .addSubcommand("start-group", new CommandLine(new 
CamelRouteGroupStartAction(this)))
                         .addSubcommand("start-route", new CommandLine(new 
CamelRouteStartAction(this)))
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSqlQueryAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSqlQueryAction.java
new file mode 100644
index 000000000000..179a7a235d4f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSqlQueryAction.java
@@ -0,0 +1,174 @@
+/*
+ * 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.dsl.jbang.core.commands.action;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import picocli.CommandLine;
+
[email protected](name = "sql",
+                     description = "Execute SQL query on a DataSource", 
sortOptions = false,
+                     showDefaultValues = true,
+                     footer = {
+                             "%nExamples:",
+                             "  camel cmd sql myApp --query=\"SELECT * FROM 
orders\"",
+                             "  camel cmd sql myApp --query=\"SELECT * FROM 
users\" --datasource=myDS --max-rows=50",
+                             "  camel cmd sql myApp 
--query=\"file:query.sql\"" })
+public class CamelSqlQueryAction extends ActionBaseCommand {
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel 
integration",
+                            arity = "1")
+    String name;
+
+    @CommandLine.Option(names = { "--query", "--sql" }, required = true,
+                        description = "The SQL query to execute, or 
file:<path> to load from a file")
+    String query;
+
+    @CommandLine.Option(names = { "--datasource" },
+                        description = "Name of the DataSource bean 
(auto-detected if only one exists)")
+    String datasource;
+
+    @CommandLine.Option(names = { "--max-rows" }, defaultValue = "100",
+                        description = "Maximum number of rows to return")
+    int maxRows = 100;
+
+    @CommandLine.Option(names = { "--query-timeout" }, defaultValue = "30",
+                        description = "Query timeout in seconds")
+    int queryTimeout = 30;
+
+    public CamelSqlQueryAction(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        List<Long> pids = findPids(name);
+        if (pids.size() != 1) {
+            printer().println("Name or pid " + name + " matches " + pids.size()
+                              + " running Camel integrations. Specify a name 
or PID that matches exactly one.");
+            return 1;
+        }
+
+        long pid = pids.get(0);
+
+        Path outputFile = getOutputFile(Long.toString(pid));
+        PathUtils.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", "sql-query");
+        root.put("sql", query);
+        if (datasource != null) {
+            root.put("datasource", datasource);
+        }
+        root.put("maxRows", maxRows);
+        root.put("queryTimeout", queryTimeout);
+
+        Path actionFile = getActionFile(Long.toString(pid));
+        Files.writeString(actionFile, root.toJson());
+
+        JsonObject jo = getJsonObject(outputFile, (queryTimeout + 10) * 1000L);
+        try {
+            if (jo == null) {
+                printer().println("Timeout waiting for SQL query response");
+                return 1;
+            }
+
+            String status = jo.getString("status");
+            if ("error".equals(status)) {
+                printer().println("Error: " + jo.getString("message"));
+                return 1;
+            }
+
+            if (jo.containsKey("updateCount")) {
+                int updateCount = jo.getInteger("updateCount");
+                long elapsed = jo.getLongOrDefault("elapsed", 0);
+                printer().println("Update count: " + updateCount);
+                printer().println("Elapsed: " + elapsed + "ms");
+                return 0;
+            }
+
+            JsonArray columns = jo.getCollection("columns");
+            JsonArray rows = jo.getCollection("rows");
+            if (columns == null || rows == null) {
+                printer().println("No result data");
+                return 0;
+            }
+
+            // compute column widths
+            String[] colNames = new String[columns.size()];
+            int[] widths = new int[columns.size()];
+            for (int i = 0; i < columns.size(); i++) {
+                JsonObject col = (JsonObject) columns.get(i);
+                colNames[i] = col.getString("name");
+                widths[i] = colNames[i].length();
+            }
+            for (Object rowObj : rows) {
+                JsonObject row = (JsonObject) rowObj;
+                for (int i = 0; i < colNames.length; i++) {
+                    Object val = row.get(colNames[i]);
+                    int len = val != null ? String.valueOf(val).length() : 4;
+                    widths[i] = Math.max(widths[i], len);
+                }
+            }
+
+            // print header
+            StringBuilder header = new StringBuilder();
+            StringBuilder separator = new StringBuilder();
+            for (int i = 0; i < colNames.length; i++) {
+                if (i > 0) {
+                    header.append(" | ");
+                    separator.append("-+-");
+                }
+                header.append(String.format("%-" + widths[i] + "s", 
colNames[i]));
+                separator.append("-".repeat(widths[i]));
+            }
+            printer().println(header.toString());
+            printer().println(separator.toString());
+
+            // print rows
+            for (Object rowObj : rows) {
+                JsonObject row = (JsonObject) rowObj;
+                StringBuilder line = new StringBuilder();
+                for (int i = 0; i < colNames.length; i++) {
+                    if (i > 0) {
+                        line.append(" | ");
+                    }
+                    Object val = row.get(colNames[i]);
+                    String s = val != null ? String.valueOf(val) : "null";
+                    line.append(String.format("%-" + widths[i] + "s", s));
+                }
+                printer().println(line.toString());
+            }
+
+            int rowCount = jo.getIntegerOrDefault("rowCount", rows.size());
+            boolean truncated = jo.getBooleanOrDefault("truncated", false);
+            long elapsed = jo.getLongOrDefault("elapsed", 0);
+            printer().println();
+            printer().println(rowCount + " row(s)" + (truncated ? " 
(truncated)" : "") + " in " + elapsed + "ms");
+        } finally {
+            PathUtils.deleteFile(outputFile);
+        }
+
+        return 0;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
index a23378060634..a492e4e57d72 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java
@@ -183,6 +183,10 @@ public final class RuntimeHelper {
      * @return           the raw response string, or a timeout message if no 
response was received
      */
     public static String executeAction(long pid, String action, 
Consumer<JsonObject> configure) {
+        return executeAction(pid, action, configure, ACTION_TIMEOUT_MS);
+    }
+
+    public static String executeAction(long pid, String action, 
Consumer<JsonObject> configure, long timeoutMs) {
         String requestId = UUID.randomUUID().toString().substring(0, 8);
         Path camelDir = CommandLineHelper.getCamelDir();
         Path outputFile = camelDir.resolve(pid + "-output-" + requestId + 
".json");
@@ -199,7 +203,7 @@ public final class RuntimeHelper {
 
         try {
             StopWatch watch = new StopWatch();
-            while (watch.taken() < ACTION_TIMEOUT_MS) {
+            while (watch.taken() < timeoutMs) {
                 try {
                     Thread.sleep(POLL_INTERVAL_MS);
                     if (Files.exists(outputFile) && 
outputFile.toFile().length() > 0) {
@@ -272,6 +276,44 @@ public final class RuntimeHelper {
         }
     }
 
+    public static JsonObject executeRowUpdate(
+            long pid, String table, String datasource, String pkValuesJson, 
String colValuesJson) {
+        String result = executeAction(pid, "sql-update-row", root -> {
+            root.put("table", table);
+            if (datasource != null) {
+                root.put("datasource", datasource);
+            }
+            root.put("primaryKeyValues", pkValuesJson);
+            root.put("columnValues", colValuesJson);
+        });
+        try {
+            return (JsonObject) Jsoner.deserialize(result);
+        } catch (Exception e) {
+            JsonObject wrapper = new JsonObject();
+            wrapper.put("result", result);
+            return wrapper;
+        }
+    }
+
+    public static JsonObject executeSqlQuery(long pid, String sql, String 
datasource, int maxRows, int queryTimeout) {
+        long timeout = (queryTimeout + 10) * 1000L;
+        String result = executeAction(pid, "sql-query", root -> {
+            root.put("sql", sql);
+            if (datasource != null) {
+                root.put("datasource", datasource);
+            }
+            root.put("maxRows", maxRows);
+            root.put("queryTimeout", queryTimeout);
+        }, timeout);
+        try {
+            return (JsonObject) Jsoner.deserialize(result);
+        } catch (Exception e) {
+            JsonObject wrapper = new JsonObject();
+            wrapper.put("result", result);
+            return wrapper;
+        }
+    }
+
     public static JsonObject readStatusFromFile(Path path) {
         try {
             if (Files.exists(path) && path.toFile().length() > 0) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/sql.camel.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/sql.camel.yaml
index 3153d25c1917..34d02a5efe2d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/sql.camel.yaml
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/sql.camel.yaml
@@ -29,7 +29,7 @@
       uri: timer
       parameters:
         timerName: select
-        delay: 5000
+        period: 5000
       steps:
         - to:
             uri: sql
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index d8626cfe95c8..23a61d45d60e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -231,6 +231,7 @@ public class CamelMonitor extends CamelCommand {
     private ProcessTab processTab;
     private OverviewTab overviewTab;
     private DataSourceTab dataSourceTab;
+    private SqlQueryTab sqlQueryTab;
 
     // "Switch integration" popup state
     private boolean showSwitchPopup;
@@ -285,6 +286,7 @@ public class CamelMonitor extends CamelCommand {
         routesTab = new RoutesTab(ctx);
         consumersTab = new ConsumersTab(ctx);
         dataSourceTab = new DataSourceTab(ctx);
+        sqlQueryTab = new SqlQueryTab(ctx);
         endpointsTab = new EndpointsTab(ctx, metrics);
         httpTab = new HttpTab(ctx);
         healthTab = new HealthTab(ctx);
@@ -444,7 +446,7 @@ public class CamelMonitor extends CamelCommand {
                 return true;
             }
             if (ke.isDown()) {
-                morePopupState.selectNext(14);
+                morePopupState.selectNext(15);
                 return true;
             }
             int shortcutSel = morePopupShortcut(ke);
@@ -467,10 +469,11 @@ public class CamelMonitor extends CamelCommand {
                         case 7 -> inflightTab;
                         case 8 -> memoryTab;
                         case 9 -> metricsTab;
-                        case 10 -> spansTab;
-                        case 11 -> processTab;
-                        case 12 -> startupTab;
-                        case 13 -> threadsTab;
+                        case 10 -> sqlQueryTab;
+                        case 11 -> spansTab;
+                        case 12 -> processTab;
+                        case 13 -> startupTab;
+                        case 14 -> threadsTab;
                         default -> null;
                     };
                     if (activeMoreTab != null) {
@@ -546,7 +549,9 @@ public class CamelMonitor extends CamelCommand {
         boolean logSearchActive = tabsState.selected() == TAB_LOG && 
logTab.isSearchInputActive();
         boolean spanFilterActive = tabsState.selected() == TAB_MORE && 
activeMoreTab == spansTab
                 && spansTab.isFilterInputActive();
-        boolean textEditing = probeEditing || logSearchActive || 
spanFilterActive;
+        boolean sqlInputActive = tabsState.selected() == TAB_MORE && 
activeMoreTab == sqlQueryTab
+                && sqlQueryTab.isInputActive();
+        boolean textEditing = probeEditing || logSearchActive || 
spanFilterActive || sqlInputActive;
         if (!textEditing && (ke.isCharIgnoreCase('q') || ke.isCtrlC())) {
             runner.quit();
             return true;
@@ -783,6 +788,10 @@ public class CamelMonitor extends CamelCommand {
             filesBrowser.handlePaste(pe.text());
             return true;
         }
+        if (activeMoreTab == sqlQueryTab && sqlQueryTab.isInputActive()) {
+            sqlQueryTab.handlePaste(pe.text());
+            return true;
+        }
         return false;
     }
 
@@ -948,6 +957,7 @@ public class CamelMonitor extends CamelCommand {
         configurationTab.onIntegrationChanged();
         consumersTab.onIntegrationChanged();
         dataSourceTab.onIntegrationChanged();
+        sqlQueryTab.onIntegrationChanged();
         circuitBreakerTab.onIntegrationChanged();
         inflightTab.onIntegrationChanged();
         spansTab.onIntegrationChanged();
@@ -1320,7 +1330,7 @@ public class CamelMonitor extends CamelCommand {
 
     private void renderMorePopup(Frame frame, Rect area) {
         int popupW = 22;
-        int popupH = 16;
+        int popupH = 17;
         // Position just below the "0 More▾" tab label
         int dividerW = CharWidth.of(" | ");
         int tabBarX = 0;
@@ -1352,6 +1362,7 @@ public class CamelMonitor extends CamelCommand {
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("I", 
keyStyle), Span.raw("nflight"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("M", 
keyStyle), Span.raw("emory"))),
                 ListItem.from(Line.from(Span.raw("  M"), Span.styled("e", 
keyStyle), Span.raw("trics"))),
+                ListItem.from(Line.from(Span.raw("  S"), Span.styled("Q", 
keyStyle), Span.raw("L Query"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("O", 
keyStyle), Span.raw("Tel Spans"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("P", 
keyStyle), Span.raw("rocess"))),
                 ListItem.from(Line.from(Span.raw("  "), Span.styled("S", 
keyStyle), Span.raw("tartup"))),
@@ -1463,18 +1474,21 @@ public class CamelMonitor extends CamelCommand {
         if (ke.isChar('e')) {
             return 9;
         }
-        if (ke.isChar('o')) {
+        if (ke.isChar('q')) {
             return 10;
         }
-        if (ke.isChar('p')) {
+        if (ke.isChar('o')) {
             return 11;
         }
-        if (ke.isChar('s')) {
+        if (ke.isChar('p')) {
             return 12;
         }
-        if (ke.isChar('t')) {
+        if (ke.isChar('s')) {
             return 13;
         }
+        if (ke.isChar('t')) {
+            return 14;
+        }
         return -1;
     }
 
@@ -2599,7 +2613,8 @@ public class CamelMonitor extends CamelCommand {
 
     private static final String[] MORE_TAB_NAMES = {
             "Beans", "Browse", "Circuit Breaker", "Classpath", "Configuration",
-            "Consumers", "DataSource", "Inflight", "Memory", "Metrics", 
"Spans", "Process", "Startup", "Threads"
+            "Consumers", "DataSource", "Inflight", "Memory", "Metrics", "SQL 
Query", "Spans", "Process", "Startup",
+            "Threads"
     };
 
     String navigateToTab(String tabName) {
@@ -2625,10 +2640,11 @@ public class CamelMonitor extends CamelCommand {
                     case 7 -> inflightTab;
                     case 8 -> memoryTab;
                     case 9 -> metricsTab;
-                    case 10 -> spansTab;
-                    case 11 -> processTab;
-                    case 12 -> startupTab;
-                    case 13 -> threadsTab;
+                    case 10 -> sqlQueryTab;
+                    case 11 -> spansTab;
+                    case 12 -> processTab;
+                    case 13 -> startupTab;
+                    case 14 -> threadsTab;
                     default -> null;
                 };
                 if (activeMoreTab != null) {
@@ -3092,6 +3108,14 @@ public class CamelMonitor extends CamelCommand {
         return tab != null && tab.setFilter(filter);
     }
 
+    boolean setTabInputValue(String tabName, String field, String value) {
+        if (tabName != null) {
+            navigateToTab(tabName);
+        }
+        MonitorTab tab = activeTab();
+        return tab != null && tab.setInputValue(field, value);
+    }
+
     String toggleTraceDisplay(String section, Boolean enabled) {
         return historyTab.toggleDisplaySection(section, enabled);
     }
@@ -3109,6 +3133,32 @@ public class CamelMonitor extends CamelCommand {
         return RuntimeHelper.sendMessage(pid, endpoint, body, headers);
     }
 
+    JsonObject executeSql(String sql, String datasource, int maxRows, int 
queryTimeout) {
+        if (ctx.selectedPid == null) {
+            return null;
+        }
+        long pid;
+        try {
+            pid = Long.parseLong(ctx.selectedPid);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+        return RuntimeHelper.executeSqlQuery(pid, sql, datasource, maxRows, 
queryTimeout);
+    }
+
+    JsonObject updateRow(String table, String datasource, String pkValuesJson, 
String colValuesJson) {
+        if (ctx.selectedPid == null) {
+            return null;
+        }
+        long pid;
+        try {
+            pid = Long.parseLong(ctx.selectedPid);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+        return RuntimeHelper.executeRowUpdate(pid, table, datasource, 
pkValuesJson, colValuesJson);
+    }
+
     String controlIntegration(String action) {
         if (action == null || action.isBlank()) {
             return "Error: action is required";
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
index ac960570239b..d9525c6ffbed 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
@@ -358,6 +358,14 @@ class ClasspathTab implements MonitorTab {
         return true;
     }
 
+    @Override
+    public boolean setInputValue(String field, String value) {
+        if ("filter".equals(field)) {
+            return setFilter(value);
+        }
+        return false;
+    }
+
     @Override
     public SelectionContext getSelectionContext() {
         return null;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
index 4f14b398e58e..e39f75cab22c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
@@ -1626,6 +1626,23 @@ class HttpTab implements MonitorTab {
                 """;
     }
 
+    @Override
+    public boolean setInputValue(String field, String value) {
+        if (!probeMode) {
+            return false;
+        }
+        String v = value != null ? value : "";
+        if ("path".equals(field)) {
+            probePathState.setText(v);
+            return true;
+        }
+        if ("body".equals(field)) {
+            probeBodyState.setText(v);
+            return true;
+        }
+        return false;
+    }
+
     @Override
     public JsonObject getTableDataAsJson() {
         IntegrationInfo info = ctx.findSelectedIntegration();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InputHistory.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InputHistory.java
new file mode 100644
index 000000000000..d132557af381
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InputHistory.java
@@ -0,0 +1,153 @@
+/*
+ * 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.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+
+/**
+ * Reusable input history with popup selector. Stores string entries and 
renders a centered popup overlay for selection.
+ */
+class InputHistory {
+
+    private static final int MAX_ENTRIES = 20;
+
+    private final List<String> entries = new ArrayList<>();
+    private final ListState listState = new ListState();
+    private boolean popupVisible;
+    private String selected;
+
+    void add(String entry) {
+        if (entry == null || entry.isBlank()) {
+            return;
+        }
+        entries.remove(entry);
+        entries.add(0, entry);
+        if (entries.size() > MAX_ENTRIES) {
+            entries.remove(entries.size() - 1);
+        }
+    }
+
+    boolean isEmpty() {
+        return entries.isEmpty();
+    }
+
+    boolean isPopupVisible() {
+        return popupVisible;
+    }
+
+    void showPopup() {
+        popupVisible = true;
+        listState.select(0);
+        selected = null;
+    }
+
+    void hidePopup() {
+        popupVisible = false;
+        selected = null;
+    }
+
+    /**
+     * Returns the entry selected on Enter, then resets. Returns null if 
nothing was selected.
+     */
+    String takeSelected() {
+        String s = selected;
+        selected = null;
+        return s;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!popupVisible) {
+            return false;
+        }
+        if (ke.isCancel()) {
+            hidePopup();
+            return true;
+        }
+        if (ke.isUp()) {
+            listState.selectPrevious();
+            return true;
+        }
+        if (ke.isDown()) {
+            listState.selectNext(entries.size());
+            return true;
+        }
+        if (ke.isConfirm()) {
+            Integer sel = listState.selected();
+            if (sel != null && sel >= 0 && sel < entries.size()) {
+                selected = entries.get(sel);
+            }
+            popupVisible = false;
+            return true;
+        }
+        return true;
+    }
+
+    void renderPopup(Frame frame, Rect area, String title) {
+        if (!popupVisible || entries.isEmpty()) {
+            return;
+        }
+
+        int maxLabelLen = 0;
+        for (String q : entries) {
+            String oneLine = q.replace('\n', ' ');
+            maxLabelLen = Math.max(maxLabelLen, oneLine.length() + 4);
+        }
+        int popupW = Math.min(area.width() - 4, Math.max(30, maxLabelLen + 4));
+        int popupH = Math.min(area.height() - 4, entries.size() + 2);
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + 2;
+        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height() - 2));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        ListItem[] items = new ListItem[entries.size()];
+        for (int i = 0; i < entries.size(); i++) {
+            String label = entries.get(i).replace('\n', ' ');
+            if (label.length() > popupW - 6) {
+                label = label.substring(0, popupW - 9) + "...";
+            }
+            items[i] = ListItem.from(Line.from(Span.raw("  " + label)));
+        }
+
+        ListWidget list = ListWidget.builder()
+                .items(items)
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSymbol("")
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(Title.from(Line.from(Span.styled(" " + title + 
" ", Style.EMPTY.fg(Color.YELLOW).bold()))))
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, listState);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
index 511679e38f1f..1eabd85d3632 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
@@ -62,4 +62,8 @@ interface MonitorTab {
     default boolean setFilter(String filter) {
         return false;
     }
+
+    default boolean setInputValue(String field, String value) {
+        return false;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
index cc314bedba23..24731224b944 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
@@ -227,6 +227,14 @@ class SpansTab implements MonitorTab {
         return true;
     }
 
+    @Override
+    public boolean setInputValue(String field, String value) {
+        if ("filter".equals(field)) {
+            return setFilter(value);
+        }
+        return false;
+    }
+
     @Override
     public void navigateUp() {
         if (!waterfallView) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
new file mode 100644
index 000000000000..f0db2c068485
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlQueryTab.java
@@ -0,0 +1,1057 @@
+/*
+ * 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.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.input.TextArea;
+import dev.tamboui.widgets.input.TextAreaState;
+import dev.tamboui.widgets.input.TextInput;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import dev.tamboui.widgets.table.TableState;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
+
+class SqlQueryTab implements MonitorTab {
+
+    private final MonitorContext ctx;
+    private final TextAreaState sqlInput = new TextAreaState();
+    private final TableState tableState = new TableState();
+    private final AtomicBoolean executing = new AtomicBoolean();
+    private final InputHistory sqlHistory = new InputHistory();
+
+    // datasource selection
+    private List<String> dsNames = new ArrayList<>();
+    private int selectedDs;
+    private boolean focusOnInput = true;
+
+    // results
+    private String[] columnNames;
+    private boolean[] columnIsPk;
+    private List<JsonObject> resultRows;
+    private int rowCount;
+    private boolean truncated;
+    private long elapsed;
+    private String errorMessage;
+    private Integer updateCount;
+
+    // editability metadata from response
+    private String tableName;
+    private String[] primaryKeys;
+
+    // edit row state
+    private boolean editMode;
+    private int editField;
+    private int editScrollOffset;
+    private TextInputState[] editInputs;
+    private String[] editOriginalValues;
+    private int editRowIndex;
+    private String editUpdateMessage;
+
+    // store last query for re-execution after update
+    private String lastSql;
+    private String lastDsName;
+
+    SqlQueryTab(MonitorContext ctx) {
+        this.ctx = ctx;
+    }
+
+    boolean isInputActive() {
+        return editMode || focusOnInput;
+    }
+
+    void handlePaste(String text) {
+        if (editMode) {
+            if (editInputs != null && editField >= 0 && editField < 
editInputs.length
+                    && !columnIsPk[editField]) {
+                FormHelper.handlePaste(text, editInputs[editField]);
+            }
+        } else if (focusOnInput) {
+            sqlInput.insert(text);
+        }
+    }
+
+    @Override
+    public void onIntegrationChanged() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        dsNames.clear();
+        selectedDs = 0;
+        if (info != null) {
+            for (DataSourceInfo ds : info.dataSources) {
+                dsNames.add(ds.name);
+            }
+        }
+        clearResults();
+        closeEditMode();
+    }
+
+    @Override
+    public void onTabSelected() {
+        focusOnInput = true;
+        onIntegrationChanged();
+    }
+
+    @Override
+    public boolean handleKeyEvent(KeyEvent ke) {
+        if (executing.get()) {
+            return true;
+        }
+
+        // edit mode takes priority
+        if (editMode) {
+            return handleEditKeyEvent(ke);
+        }
+
+        // history popup takes priority
+        if (sqlHistory.isPopupVisible()) {
+            sqlHistory.handleKeyEvent(ke);
+            String selected = sqlHistory.takeSelected();
+            if (selected != null) {
+                sqlInput.clear();
+                sqlInput.insert(selected);
+            }
+            return true;
+        }
+
+        if (ke.isCancel()) {
+            if (!focusOnInput && resultRows != null) {
+                focusOnInput = true;
+                return true;
+            }
+            return false;
+        }
+
+        // F5 to execute query
+        if (focusOnInput && ke.code() == KeyCode.F5) {
+            executeQuery();
+            return true;
+        }
+
+        // Tab to toggle focus between input and results
+        if (ke.code() == KeyCode.TAB && resultRows != null && 
!resultRows.isEmpty()) {
+            focusOnInput = !focusOnInput;
+            return true;
+        }
+
+        // Ctrl+E to open history popup
+        if (focusOnInput && ke.hasCtrl() && ke.isCharIgnoreCase('e') && 
!sqlHistory.isEmpty()) {
+            sqlHistory.showPopup();
+            return true;
+        }
+
+        // Enter: newline in input, or open edit in results
+        if (ke.isConfirm()) {
+            if (focusOnInput) {
+                sqlInput.insert('\n');
+                return true;
+            }
+            // results mode: open edit if editable
+            if (isEditable()) {
+                openEditMode();
+            }
+            return true;
+        }
+
+        if (focusOnInput) {
+            // datasource cycling with Ctrl+Left/Right
+            if (ke.hasCtrl() && ke.isLeft() && dsNames.size() > 1) {
+                selectedDs = (selectedDs - 1 + dsNames.size()) % 
dsNames.size();
+                return true;
+            }
+            if (ke.hasCtrl() && ke.isRight() && dsNames.size() > 1) {
+                selectedDs = (selectedDs + 1) % dsNames.size();
+                return true;
+            }
+
+            // cursor movement
+            if (ke.isUp()) {
+                sqlInput.moveCursorUp();
+                return true;
+            }
+            if (ke.isDown()) {
+                sqlInput.moveCursorDown();
+                return true;
+            }
+            if (ke.isLeft()) {
+                sqlInput.moveCursorLeft();
+                return true;
+            }
+            if (ke.isRight()) {
+                sqlInput.moveCursorRight();
+                return true;
+            }
+            if (ke.isHome()) {
+                sqlInput.moveCursorToLineStart();
+                return true;
+            }
+            if (ke.isEnd()) {
+                sqlInput.moveCursorToLineEnd();
+                return true;
+            }
+            if (ke.isDeleteBackward()) {
+                sqlInput.deleteBackward();
+                return true;
+            }
+            if (ke.isDeleteForward()) {
+                sqlInput.deleteForward();
+                return true;
+            }
+
+            // typed character
+            if (ke.code() == KeyCode.CHAR) {
+                sqlInput.insert(ke.character());
+                return true;
+            }
+
+            return true;
+        }
+
+        // results navigation
+        if (ke.isUp()) {
+            navigateUp();
+            return true;
+        }
+        if (ke.isDown()) {
+            navigateDown();
+            return true;
+        }
+
+        return true;
+    }
+
+    private boolean handleEditKeyEvent(KeyEvent ke) {
+        if (ke.isCancel()) {
+            closeEditMode();
+            return true;
+        }
+
+        // F5 to save changes
+        if (ke.code() == KeyCode.F5) {
+            saveEditedRow();
+            return true;
+        }
+
+        // navigate fields
+        if (ke.isUp()) {
+            moveEditField(-1);
+            return true;
+        }
+        if (ke.isDown() || ke.code() == KeyCode.TAB) {
+            moveEditField(1);
+            return true;
+        }
+
+        // edit current field (only non-PK)
+        if (editField >= 0 && editField < editInputs.length && 
!columnIsPk[editField]) {
+            FormHelper.handleTextInput(ke, editInputs[editField]);
+        }
+        return true;
+    }
+
+    private void moveEditField(int direction) {
+        int count = columnNames.length;
+        int next = editField;
+        for (int i = 0; i < count; i++) {
+            next = (next + direction + count) % count;
+            if (!columnIsPk[next]) {
+                editField = next;
+                return;
+            }
+        }
+    }
+
+    @Override
+    public boolean handleEscape() {
+        if (editMode) {
+            closeEditMode();
+            return true;
+        }
+        if (sqlHistory.isPopupVisible()) {
+            sqlHistory.hidePopup();
+            return true;
+        }
+        if (!focusOnInput && resultRows != null) {
+            focusOnInput = true;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void navigateUp() {
+        if (!focusOnInput) {
+            tableState.selectPrevious();
+        }
+    }
+
+    @Override
+    public void navigateDown() {
+        if (!focusOnInput && resultRows != null) {
+            tableState.selectNext(resultRows.size());
+        }
+    }
+
+    @Override
+    public void render(Frame frame, Rect area) {
+        if (area.height() < 4) {
+            return;
+        }
+
+        // layout: datasource bar (1 line) + SQL input (5 lines) + results 
(rest)
+        int inputH = 5;
+        int dsBarH = dsNames.size() > 1 ? 1 : 0;
+        int topH = dsBarH + inputH;
+
+        List<Rect> parts = Layout.vertical()
+                .constraints(Constraint.length(topH), Constraint.min(3))
+                .split(area);
+
+        renderInputArea(frame, parts.get(0), dsBarH);
+        renderResults(frame, parts.get(1));
+
+        sqlHistory.renderPopup(frame, area, "Query History");
+
+        if (editMode) {
+            renderEditPopup(frame, area);
+        }
+    }
+
+    private void renderInputArea(Frame frame, Rect area, int dsBarH) {
+        if (dsBarH > 0 && area.height() > 0) {
+            Rect dsBar = new Rect(area.x(), area.y(), area.width(), 1);
+            StringBuilder sb = new StringBuilder(" DataSource: ");
+            for (int i = 0; i < dsNames.size(); i++) {
+                if (i == selectedDs) {
+                    sb.append("[").append(dsNames.get(i)).append("]");
+                } else {
+                    sb.append(" ").append(dsNames.get(i)).append(" ");
+                }
+                if (i < dsNames.size() - 1) {
+                    sb.append("  ");
+                }
+            }
+            Style style = focusOnInput ? Style.EMPTY.fg(Color.CYAN) : 
Style.EMPTY.fg(Color.DARK_GRAY);
+            Paragraph dsLabel = Paragraph.builder()
+                    .text(Text.from(Line.from(Span.styled(sb.toString(), 
style))))
+                    .build();
+            frame.renderWidget(dsLabel, dsBar);
+        }
+
+        Rect inputRect = new Rect(area.x(), area.y() + dsBarH, area.width(), 
area.height() - dsBarH);
+
+        String title;
+        if (executing.get()) {
+            title = " Executing... ";
+        } else if (errorMessage != null) {
+            title = " SQL Query (error) ";
+        } else if (updateCount != null) {
+            title = String.format(" SQL Query (%d updated, %dms) ", 
updateCount, elapsed);
+        } else if (resultRows != null) {
+            title = String.format(" SQL Query (%d row(s), %dms) ", rowCount, 
elapsed);
+        } else {
+            title = " SQL Query (F5 to execute) ";
+        }
+        Style borderStyle = focusOnInput ? Style.EMPTY.fg(Color.CYAN) : 
Style.EMPTY.fg(Color.DARK_GRAY);
+        Block inputBlock = Block.builder()
+                .title(Title.from(title))
+                .borderType(BorderType.ROUNDED)
+                .borderStyle(borderStyle)
+                .build();
+        Rect inner = inputBlock.inner(inputRect);
+        frame.renderWidget(inputBlock, inputRect);
+
+        TextArea textArea = TextArea.builder()
+                .cursorStyle(Style.EMPTY.reversed())
+                .placeholder("Type SQL query or file:query.sql ...")
+                .build();
+        if (focusOnInput) {
+            textArea.renderWithCursor(inner, frame.buffer(), sqlInput, frame);
+        } else {
+            textArea.render(inner, frame.buffer(), sqlInput);
+        }
+    }
+
+    private void renderResults(Frame frame, Rect area) {
+        if (errorMessage != null) {
+            Block errBlock = Block.builder()
+                    .title(Title.from(" Error "))
+                    .borderType(BorderType.ROUNDED)
+                    .borderStyle(Style.EMPTY.fg(Color.RED))
+                    .build();
+            Rect inner = errBlock.inner(area);
+            frame.renderWidget(errBlock, area);
+            Paragraph errText = Paragraph.builder()
+                    .text(Text.from(Line.from(Span.styled(errorMessage, 
Style.EMPTY.fg(Color.RED)))))
+                    .build();
+            frame.renderWidget(errText, inner);
+            return;
+        }
+
+        if (updateCount != null) {
+            Block ucBlock = Block.builder()
+                    .title(Title.from(" Result "))
+                    .borderType(BorderType.ROUNDED)
+                    .borderStyle(Style.EMPTY.fg(Color.GREEN))
+                    .build();
+            Rect inner = ucBlock.inner(area);
+            frame.renderWidget(ucBlock, area);
+            String msg = String.format("Update count: %d  (%dms)", 
updateCount, elapsed);
+            Paragraph ucText = Paragraph.builder()
+                    .text(Text.from(Line.from(Span.styled(msg, 
Style.EMPTY.fg(Color.GREEN)))))
+                    .build();
+            frame.renderWidget(ucText, inner);
+            return;
+        }
+
+        if (columnNames == null || resultRows == null) {
+            Block emptyBlock = Block.builder()
+                    .title(Title.from(" Results "))
+                    .borderType(BorderType.ROUNDED)
+                    .borderStyle(Style.EMPTY.fg(Color.DARK_GRAY))
+                    .build();
+            Rect inner = emptyBlock.inner(area);
+            frame.renderWidget(emptyBlock, area);
+
+            String hint = dsNames.isEmpty()
+                    ? "No DataSource available"
+                    : "Type a SQL query and press F5 to execute";
+            Paragraph hintText = Paragraph.builder()
+                    .text(Text.from(Line.from(Span.styled(hint, 
Style.EMPTY.fg(Color.DARK_GRAY)))))
+                    .build();
+            frame.renderWidget(hintText, inner);
+            return;
+        }
+
+        // build result table
+        String resultTitle = String.format(" %d row(s)%s  %dms ",
+                rowCount, truncated ? " (truncated)" : "", elapsed);
+        Style tableBorderStyle = !focusOnInput ? Style.EMPTY.fg(Color.CYAN) : 
Style.EMPTY.fg(Color.DARK_GRAY);
+        Block tableBlock = Block.builder()
+                .title(Title.from(resultTitle))
+                .borderType(BorderType.ROUNDED)
+                .borderStyle(tableBorderStyle)
+                .build();
+
+        int[] widths = computeColumnWidths(area.width() - 2);
+
+        Row header = Row.from(buildHeaderCells());
+        header.style(Style.EMPTY.fg(Color.YELLOW));
+
+        List<Row> dataRows = new ArrayList<>();
+        for (JsonObject row : resultRows) {
+            List<Cell> cells = new ArrayList<>();
+            for (String col : columnNames) {
+                Object val = row.get(col);
+                String s = val != null ? String.valueOf(val) : "null";
+                Style style = val == null ? Style.EMPTY.fg(Color.DARK_GRAY) : 
Style.EMPTY.fg(Color.WHITE);
+                cells.add(Cell.from(Span.styled(s, style)));
+            }
+            dataRows.add(Row.from(cells));
+        }
+
+        Constraint[] colConstraints = new Constraint[widths.length];
+        for (int i = 0; i < widths.length; i++) {
+            colConstraints[i] = Constraint.length(widths[i]);
+        }
+
+        Table table = Table.builder()
+                .header(header)
+                .rows(dataRows)
+                .widths(colConstraints)
+                .block(tableBlock)
+                .highlightStyle(Style.EMPTY.bg(Color.DARK_GRAY))
+                .build();
+        frame.renderStatefulWidget(table, area, tableState);
+    }
+
+    private Cell[] buildHeaderCells() {
+        Cell[] cells = new Cell[columnNames.length];
+        for (int i = 0; i < columnNames.length; i++) {
+            String label = columnNames[i];
+            if (columnIsPk != null && columnIsPk[i]) {
+                label = label + " 🔑";
+            }
+            cells[i] = Cell.from(Span.styled(label, 
Style.EMPTY.fg(Color.YELLOW)));
+        }
+        return cells;
+    }
+
+    private int[] computeColumnWidths(int availableWidth) {
+        int colCount = columnNames.length;
+        int[] widths = new int[colCount];
+
+        for (int i = 0; i < colCount; i++) {
+            widths[i] = columnNames[i].length();
+            if (columnIsPk != null && columnIsPk[i]) {
+                widths[i] += 3;
+            }
+        }
+        if (resultRows != null) {
+            for (JsonObject row : resultRows) {
+                for (int i = 0; i < colCount; i++) {
+                    Object val = row.get(columnNames[i]);
+                    int len = val != null ? String.valueOf(val).length() : 4;
+                    widths[i] = Math.max(widths[i], len);
+                }
+            }
+        }
+
+        // cap each column to reasonable max
+        int maxColWidth = Math.max(10, availableWidth / Math.max(1, colCount));
+        for (int i = 0; i < colCount; i++) {
+            widths[i] = Math.min(widths[i] + 2, maxColWidth);
+        }
+        return widths;
+    }
+
+    // ---- Edit row popup ----
+
+    private boolean isEditable() {
+        return tableName != null && primaryKeys != null && primaryKeys.length 
> 0
+                && resultRows != null && !resultRows.isEmpty();
+    }
+
+    private void openEditMode() {
+        Integer sel = tableState.selected();
+        if (sel == null || sel < 0 || sel >= resultRows.size()) {
+            return;
+        }
+        editRowIndex = sel;
+        JsonObject row = resultRows.get(sel);
+
+        editInputs = new TextInputState[columnNames.length];
+        editOriginalValues = new String[columnNames.length];
+        for (int i = 0; i < columnNames.length; i++) {
+            Object val = row.get(columnNames[i]);
+            String strVal = val != null ? String.valueOf(val) : "";
+            editOriginalValues[i] = strVal;
+            editInputs[i] = new TextInputState(strVal);
+        }
+
+        // focus on first non-PK field
+        editField = 0;
+        for (int i = 0; i < columnNames.length; i++) {
+            if (!columnIsPk[i]) {
+                editField = i;
+                break;
+            }
+        }
+        editScrollOffset = 0;
+        editUpdateMessage = null;
+        editMode = true;
+    }
+
+    private void closeEditMode() {
+        editMode = false;
+        editInputs = null;
+        editOriginalValues = null;
+        editUpdateMessage = null;
+    }
+
+    private void saveEditedRow() {
+        if (!editMode || editInputs == null || executing.get()) {
+            return;
+        }
+
+        // build changed columns
+        JsonObject colValues = new JsonObject();
+        for (int i = 0; i < columnNames.length; i++) {
+            if (columnIsPk[i]) {
+                continue;
+            }
+            String newVal = editInputs[i].text();
+            if (!newVal.equals(editOriginalValues[i])) {
+                if (newVal.isEmpty()) {
+                    colValues.put(columnNames[i], null);
+                } else {
+                    colValues.put(columnNames[i], newVal);
+                }
+            }
+        }
+
+        if (colValues.isEmpty()) {
+            editUpdateMessage = "No changes";
+            return;
+        }
+
+        // build PK values from the original row
+        JsonObject pkValues = new JsonObject();
+        JsonObject row = resultRows.get(editRowIndex);
+        for (String pk : primaryKeys) {
+            Object val = row.get(pk);
+            if (val != null) {
+                pkValues.put(pk, val);
+            } else {
+                pkValues.put(pk, null);
+            }
+        }
+
+        if (!executing.compareAndSet(false, true)) {
+            return;
+        }
+
+        String dsName = dsNames.isEmpty() ? null : dsNames.get(selectedDs);
+        String pkJson = pkValues.toJson();
+        String colJson = colValues.toJson();
+        String savedSql = lastSql;
+        String savedDs = lastDsName;
+
+        ctx.runner.scheduler().execute(() -> {
+            try {
+                Path outputFile = ctx.getOutputFile(ctx.selectedPid);
+                PathUtils.deleteFile(outputFile);
+
+                JsonObject action = new JsonObject();
+                action.put("action", "sql-update-row");
+                action.put("table", tableName);
+                if (dsName != null) {
+                    action.put("datasource", dsName);
+                }
+                action.put("primaryKeyValues", pkJson);
+                action.put("columnValues", colJson);
+
+                Path actionFile = ctx.getActionFile(ctx.selectedPid);
+                PathUtils.writeTextSafely(action.toJson(), actionFile);
+
+                JsonObject jo = pollJsonResponse(outputFile, 15000);
+                PathUtils.deleteFile(outputFile);
+
+                if (jo == null) {
+                    if (ctx.runner != null) {
+                        ctx.runner.runOnRenderThread(() -> editUpdateMessage = 
"Timeout");
+                    }
+                    return;
+                }
+
+                String status = jo.getString("status");
+                if ("error".equals(status)) {
+                    String msg = jo.getString("message");
+                    if (ctx.runner != null) {
+                        ctx.runner.runOnRenderThread(() -> editUpdateMessage = 
"Error: " + msg);
+                    }
+                    return;
+                }
+
+                int uc = jo.getIntegerOrDefault("updateCount", 0);
+                if (ctx.runner != null) {
+                    ctx.runner.runOnRenderThread(() -> {
+                        editUpdateMessage = uc + " row(s) updated";
+                        closeEditMode();
+                    });
+                }
+
+                // re-execute the original query to refresh results
+                if (savedSql != null) {
+                    executeInBackground(ctx.selectedPid, savedSql, savedDs);
+                }
+            } finally {
+                executing.set(false);
+            }
+        });
+    }
+
+    private void renderEditPopup(Frame frame, Rect area) {
+        int colCount = columnNames.length;
+        int labelW = 0;
+        for (String col : columnNames) {
+            labelW = Math.max(labelW, col.length());
+        }
+        labelW += 4;
+
+        int popupW = Math.min(area.width() - 4, Math.max(50, labelW + 30));
+        int visibleRows = Math.min(colCount, area.height() - 6);
+        int popupH = visibleRows + 4;
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + Math.max(1, (area.height() - popupH) / 2);
+        Rect popup = new Rect(x, y, popupW, popupH);
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        String title = " Edit Row — " + tableName + " ";
+        if (editUpdateMessage != null) {
+            title = " " + editUpdateMessage + " ";
+        }
+
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(Title.from(Line.from(Span.styled(title, 
Style.EMPTY.fg(Color.YELLOW).bold()))))
+                .build();
+        Rect inner = block.inner(popup);
+        frame.renderWidget(block, popup);
+
+        // adjust scroll to keep focused field visible
+        if (editField < editScrollOffset) {
+            editScrollOffset = editField;
+        } else if (editField >= editScrollOffset + visibleRows) {
+            editScrollOffset = editField - visibleRows + 1;
+        }
+
+        int fieldW = inner.width() - labelW - 1;
+        int end = Math.min(editScrollOffset + visibleRows, colCount);
+        for (int i = editScrollOffset; i < end; i++) {
+            int row = inner.top() + (i - editScrollOffset);
+            boolean isPk = columnIsPk[i];
+            boolean isFocused = (i == editField);
+
+            // column label
+            String label = columnNames[i] + (isPk ? " *" : "  ");
+            Style labelStyle;
+            if (isPk) {
+                labelStyle = Style.EMPTY.fg(Color.DARK_GRAY);
+            } else if (isFocused) {
+                labelStyle = Style.EMPTY.fg(Color.CYAN).bold();
+            } else {
+                labelStyle = Style.EMPTY;
+            }
+            Rect labelArea = new Rect(inner.left(), row, labelW, 1);
+            frame.renderWidget(Paragraph.from(Line.from(
+                    Span.styled(String.format("%" + (labelW - 1) + "s", label) 
+ " ", labelStyle))), labelArea);
+
+            // value field
+            Rect valArea = new Rect(inner.left() + labelW, row, fieldW, 1);
+            if (isPk) {
+                String val = editOriginalValues[i];
+                frame.renderWidget(Paragraph.from(Line.from(
+                        Span.styled(val.isEmpty() ? "null" : val, 
Style.EMPTY.fg(Color.DARK_GRAY)))), valArea);
+            } else if (isFocused) {
+                boolean changed = 
!editInputs[i].text().equals(editOriginalValues[i]);
+                Style cursorStyle = changed ? 
Style.EMPTY.reversed().fg(Color.GREEN) : Style.EMPTY.reversed();
+                TextInput input = TextInput.builder()
+                        .cursorStyle(cursorStyle)
+                        .build();
+                frame.renderStatefulWidget(input, valArea, editInputs[i]);
+            } else {
+                String val = editInputs[i].text();
+                boolean changed = !val.equals(editOriginalValues[i]);
+                Style valStyle = changed ? Style.EMPTY.fg(Color.GREEN) : 
Style.EMPTY;
+                frame.renderWidget(Paragraph.from(Line.from(
+                        Span.styled(val.isEmpty() ? "null" : val, valStyle))), 
valArea);
+            }
+        }
+
+        // footer hint inside popup
+        int footerY = inner.top() + visibleRows + 1;
+        if (footerY < popup.bottom() - 1) {
+            Rect footerArea = new Rect(inner.left(), footerY, inner.width(), 
1);
+            frame.renderWidget(Paragraph.from(Line.from(
+                    Span.styled(" F5", Style.EMPTY.fg(Color.YELLOW)),
+                    Span.styled("=Save  ", Style.EMPTY.fg(Color.DARK_GRAY)),
+                    Span.styled("Esc", Style.EMPTY.fg(Color.YELLOW)),
+                    Span.styled("=Cancel  ", Style.EMPTY.fg(Color.DARK_GRAY)),
+                    Span.styled("*", Style.EMPTY.fg(Color.DARK_GRAY)),
+                    Span.styled("=Primary Key", 
Style.EMPTY.fg(Color.DARK_GRAY)))), footerArea);
+        }
+    }
+
+    @Override
+    public void renderFooter(List<Span> spans) {
+        if (editMode) {
+            hint(spans, "F5", "save");
+            hint(spans, "Esc", "cancel");
+        } else if (focusOnInput) {
+            hint(spans, "F5", "execute");
+            if (!sqlHistory.isEmpty()) {
+                hint(spans, "C-e", "history");
+            }
+            if (dsNames.size() > 1) {
+                hint(spans, "C-←→", "datasource");
+            }
+            if (resultRows != null && !resultRows.isEmpty()) {
+                hint(spans, "Tab", "results");
+            }
+        } else {
+            hint(spans, "Tab", "input");
+            hint(spans, "↑↓", "navigate");
+            if (isEditable()) {
+                hint(spans, "Enter", "edit");
+            }
+        }
+    }
+
+    @Override
+    public String getHelpText() {
+        return """
+                # SQL Query
+
+                Execute SQL queries against DataSource beans registered in the 
Camel application.
+
+                ## Usage
+                - Type a SQL query in the input field and press **F5** to 
execute
+                - Type **file:query.sql** to load SQL from a file
+                - Use **Enter** for new lines in the query
+                - Use **Up/Down** arrows to move cursor within the query
+                - Paste multi-line queries from clipboard
+                - Use **Ctrl+E** to open query history (select with Enter, 
dismiss with Esc)
+                - Use **Tab** to toggle focus between input and results table
+                - Use **Esc** to return focus to the input field from results
+                - Use **Ctrl+Left/Right** to switch between DataSources (when 
multiple exist)
+
+                ## Inline Editing
+                - For simple single-table SELECT queries, press **Enter** on a 
result row to edit
+                - Primary key columns (marked with *) are read-only
+                - Changed values are highlighted in green
+                - Press **F5** to save changes (executes an UPDATE statement)
+                - Press **Esc** to cancel editing
+                - The query is automatically re-executed after a successful 
update
+
+                ## Supported Queries
+                - SELECT queries return a result table
+                - INSERT, UPDATE, DELETE return an update count
+                - Any valid SQL supported by the underlying database
+
+                ## Safety
+                - Results are limited to 100 rows by default
+                - Query timeout is 30 seconds by default
+                - This feature is only available when dev console is enabled 
(dev profile)
+                """;
+    }
+
+    @Override
+    public boolean setInputValue(String field, String value) {
+        if ("sql".equals(field)) {
+            sqlInput.setText(value != null ? value : "");
+            focusOnInput = true;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public JsonObject getTableDataAsJson() {
+        JsonObject root = new JsonObject();
+
+        // current input state
+        String sql = sqlInput.text().trim();
+        if (!sql.isEmpty()) {
+            root.put("sql", sql);
+        }
+        if (!dsNames.isEmpty()) {
+            root.put("datasource", dsNames.get(selectedDs));
+        }
+
+        if (columnNames != null && resultRows != null) {
+            JsonArray cols = new JsonArray();
+            for (String col : columnNames) {
+                cols.add(col);
+            }
+            root.put("columns", cols);
+            root.put("rows", new JsonArray(resultRows));
+            root.put("rowCount", rowCount);
+            root.put("truncated", truncated);
+            root.put("elapsed", elapsed);
+            Integer sel = tableState.selected();
+            if (sel != null && sel >= 0 && sel < resultRows.size()) {
+                root.put("selectedIndex", sel);
+            }
+            if (tableName != null) {
+                root.put("tableName", tableName);
+                root.put("editable", true);
+                if (primaryKeys != null) {
+                    JsonArray pkArr = new JsonArray();
+                    for (String pk : primaryKeys) {
+                        pkArr.add(pk);
+                    }
+                    root.put("primaryKeys", pkArr);
+                }
+            }
+        }
+        if (errorMessage != null) {
+            root.put("error", errorMessage);
+        }
+        if (updateCount != null) {
+            root.put("updateCount", updateCount);
+        }
+        if (executing.get()) {
+            root.put("executing", true);
+        }
+        return root;
+    }
+
+    private void executeQuery() {
+        String sql = sqlInput.text().trim();
+        if (sql.isEmpty() || ctx.selectedPid == null || ctx.runner == null) {
+            return;
+        }
+
+        if (!executing.compareAndSet(false, true)) {
+            return;
+        }
+
+        clearResults();
+
+        sqlHistory.add(sql);
+        String pid = ctx.selectedPid;
+        String dsName = dsNames.isEmpty() ? null : dsNames.get(selectedDs);
+        lastSql = sql;
+        lastDsName = dsName;
+
+        ctx.runner.scheduler().execute(() -> {
+            try {
+                executeInBackground(pid, sql, dsName);
+            } finally {
+                executing.set(false);
+            }
+        });
+    }
+
+    private void executeInBackground(String pid, String sql, String dsName) {
+        Path outputFile = ctx.getOutputFile(pid);
+        PathUtils.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", "sql-query");
+        root.put("sql", sql);
+        if (dsName != null) {
+            root.put("datasource", dsName);
+        }
+        root.put("maxRows", 100);
+        root.put("queryTimeout", 30);
+
+        Path actionFile = ctx.getActionFile(pid);
+        PathUtils.writeTextSafely(root.toJson(), actionFile);
+
+        JsonObject jo = pollJsonResponse(outputFile, 35000);
+        PathUtils.deleteFile(outputFile);
+
+        if (jo == null) {
+            if (ctx.runner != null) {
+                ctx.runner.runOnRenderThread(() -> {
+                    errorMessage = "Timeout waiting for query response";
+                });
+            }
+            return;
+        }
+
+        String status = jo.getString("status");
+        if ("error".equals(status)) {
+            String msg = jo.getString("message");
+            if (ctx.runner != null) {
+                ctx.runner.runOnRenderThread(() -> {
+                    errorMessage = msg;
+                    elapsed = jo.getLongOrDefault("elapsed", 0);
+                });
+            }
+            return;
+        }
+
+        // update count result
+        if (jo.containsKey("updateCount")) {
+            int uc = jo.getInteger("updateCount");
+            long el = jo.getLongOrDefault("elapsed", 0);
+            if (ctx.runner != null) {
+                ctx.runner.runOnRenderThread(() -> {
+                    updateCount = uc;
+                    elapsed = el;
+                    focusOnInput = true;
+                });
+            }
+            return;
+        }
+
+        // SELECT result
+        JsonArray columns = jo.getCollection("columns");
+        JsonArray rows = jo.getCollection("rows");
+        if (columns == null || rows == null) {
+            return;
+        }
+
+        String[] cols = new String[columns.size()];
+        boolean[] isPk = new boolean[columns.size()];
+        for (int i = 0; i < columns.size(); i++) {
+            JsonObject col = (JsonObject) columns.get(i);
+            cols[i] = col.getString("name");
+            isPk[i] = col.getBooleanOrDefault("primaryKey", false);
+        }
+
+        List<JsonObject> parsedRows = new ArrayList<>();
+        for (Object rowObj : rows) {
+            parsedRows.add((JsonObject) rowObj);
+        }
+
+        int rc = jo.getIntegerOrDefault("rowCount", parsedRows.size());
+        boolean trunc = jo.getBooleanOrDefault("truncated", false);
+        long el = jo.getLongOrDefault("elapsed", 0);
+
+        // editability metadata
+        String tblName = jo.getString("tableName");
+        String[] pks = null;
+        if (tblName != null) {
+            JsonArray pkArr = jo.getCollection("primaryKeys");
+            if (pkArr != null && !pkArr.isEmpty()) {
+                pks = new String[pkArr.size()];
+                for (int i = 0; i < pkArr.size(); i++) {
+                    pks[i] = String.valueOf(pkArr.get(i));
+                }
+            }
+        }
+
+        String[] finalPks = pks;
+        if (ctx.runner != null) {
+            ctx.runner.runOnRenderThread(() -> {
+                columnNames = cols;
+                columnIsPk = isPk;
+                resultRows = parsedRows;
+                rowCount = rc;
+                truncated = trunc;
+                elapsed = el;
+                tableName = tblName;
+                primaryKeys = finalPks;
+                tableState.select(0);
+                focusOnInput = true;
+            });
+        }
+    }
+
+    private void clearResults() {
+        columnNames = null;
+        columnIsPk = null;
+        resultRows = null;
+        rowCount = 0;
+        truncated = false;
+        elapsed = 0;
+        errorMessage = null;
+        updateCount = null;
+        tableName = null;
+        primaryKeys = null;
+        tableState.select(0);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
index fb871183452f..73a522768b11 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
@@ -443,6 +443,33 @@ class TuiMcpServer {
                         "body", propDef("string", "Message body to send"),
                         "headers", propDef("string", "Message headers as 
key=value pairs separated by newlines")),
                 List.of("endpoint")));
+        toolList.add(toolDef(
+                "tui_execute_sql",
+                "Executes a SQL query against a DataSource in the selected 
integration. "
+                                   + "Returns structured JSON with columns, 
rows, and metadata for SELECT queries, "
+                                   + "or an update count for 
INSERT/UPDATE/DELETE. "
+                                   + "Requires dev console to be enabled in 
the running application.",
+                Map.of("query", propDef("string", "The SQL query to execute"),
+                        "datasource", propDef("string",
+                                "Name of the DataSource bean (auto-detected if 
only one exists)"),
+                        "maxRows", propDef("integer",
+                                "Maximum number of rows to return (default 
100)"),
+                        "queryTimeout", propDef("integer",
+                                "Query timeout in seconds (default 30)")),
+                List.of("query")));
+        toolList.add(toolDef(
+                "tui_update_row",
+                "Updates a single row in a database table. Use after 
tui_execute_sql returns "
+                                  + "editable=true with tableName and 
primaryKeys. Builds and executes "
+                                  + "an UPDATE statement using 
PreparedStatement for safety.",
+                Map.of("table", propDef("string", "The table name to update"),
+                        "primaryKeyValues", propDef("string",
+                                "JSON object of primary key column-value 
pairs, e.g. {\"id\": 1}"),
+                        "columnValues", propDef("string",
+                                "JSON object of column-value pairs to update, 
e.g. {\"name\": \"new\"}"),
+                        "datasource", propDef("string",
+                                "Name of the DataSource bean (auto-detected if 
only one exists)")),
+                List.of("table", "primaryKeyValues", "columnValues")));
         toolList.add(toolDef(
                 "tui_set_log_level",
                 "Changes the runtime log level of the selected integration. "
@@ -461,6 +488,20 @@ class TuiMcpServer {
                         "tab", propDef("string",
                                 "Tab name to filter (e.g. 'Classpath'). If 
omitted, uses the active tab.")),
                 List.of("filter")));
+        toolList.add(toolDef(
+                "tui_set_input",
+                "Sets the value of a text input field on a TUI tab directly, 
without simulating keystrokes. "
+                                 + "The text appears in the TUI input widget 
so the user can see it. "
+                                 + "Supported fields by tab: SQL Query 
(field='sql'), "
+                                 + "HTTP probe (field='path' or 'body'), "
+                                 + "Spans (field='filter'), Classpath 
(field='filter').",
+                Map.of("field", propDef("string",
+                        "Field name to set: 'sql', 'path', 'body', or 
'filter'"),
+                        "value", propDef("string",
+                                "The text value to set in the input field"),
+                        "tab", propDef("string",
+                                "Tab name (e.g. 'SQL Query', 'HTTP'). If 
omitted, uses the active tab.")),
+                List.of("field", "value")));
         toolList.add(toolDef(
                 "tui_toggle_trace_display",
                 "Toggles which sections are visible in the History tab's 
detail view. "
@@ -565,8 +606,11 @@ class TuiMcpServer {
                 case "tui_get_history" -> callGetHistory(args);
                 case "tui_get_topology" -> callGetTopology();
                 case "tui_send_message" -> callSendMessage(args);
+                case "tui_execute_sql" -> callExecuteSql(args);
+                case "tui_update_row" -> callUpdateRow(args);
                 case "tui_set_log_level" -> callSetLogLevel(args);
                 case "tui_filter" -> callFilter(args);
+                case "tui_set_input" -> callSetInput(args);
                 case "tui_toggle_trace_display" -> 
callToggleTraceDisplay(args);
                 case "tui_get_readme" -> callGetReadme(args);
                 case "tui_control" -> callControl(args);
@@ -1186,6 +1230,42 @@ class TuiMcpServer {
         return Jsoner.serialize(response);
     }
 
+    private String callExecuteSql(Map<String, Object> args) {
+        String query = (String) args.get("query");
+        if (query == null || query.isBlank()) {
+            return "Error: query is required";
+        }
+        String datasource = args.get("datasource") instanceof String s ? s : 
null;
+        int maxRows = args.get("maxRows") instanceof Number n ? n.intValue() : 
100;
+        int queryTimeout = args.get("queryTimeout") instanceof Number n ? 
n.intValue() : 30;
+        JsonObject response = monitor.executeSql(query, datasource, maxRows, 
queryTimeout);
+        if (response == null) {
+            return "Error: no integration selected or PID unavailable";
+        }
+        return Jsoner.serialize(response);
+    }
+
+    private String callUpdateRow(Map<String, Object> args) {
+        String table = (String) args.get("table");
+        if (table == null || table.isBlank()) {
+            return "Error: table is required";
+        }
+        String pkValues = (String) args.get("primaryKeyValues");
+        if (pkValues == null || pkValues.isBlank()) {
+            return "Error: primaryKeyValues is required (JSON object)";
+        }
+        String colValues = (String) args.get("columnValues");
+        if (colValues == null || colValues.isBlank()) {
+            return "Error: columnValues is required (JSON object)";
+        }
+        String datasource = args.get("datasource") instanceof String s ? s : 
null;
+        JsonObject response = monitor.updateRow(table, datasource, pkValues, 
colValues);
+        if (response == null) {
+            return "Error: no integration selected or PID unavailable";
+        }
+        return Jsoner.serialize(response);
+    }
+
     private String callSetLogLevel(Map<String, Object> args) {
         String level = (String) args.get("level");
         if (level == null || level.isBlank()) {
@@ -1210,6 +1290,20 @@ class TuiMcpServer {
         return filter.isEmpty() ? "Filter cleared" : "Filter set to: " + 
filter;
     }
 
+    private String callSetInput(Map<String, Object> args) {
+        String field = (String) args.get("field");
+        if (field == null || field.isBlank()) {
+            return "Error: field is required";
+        }
+        String value = args.get("value") instanceof String s ? s : "";
+        String tab = args.get("tab") instanceof String s ? s : null;
+        boolean applied = monitor.setTabInputValue(tab, field, value);
+        if (!applied) {
+            return "Error: field '" + field + "' not found on " + (tab != null 
? tab : "active") + " tab";
+        }
+        return "Input set: " + field + " = " + (value.length() > 80 ? 
value.substring(0, 80) + "..." : value);
+    }
+
     private String callToggleTraceDisplay(Map<String, Object> args) {
         String section = (String) args.get("section");
         if (section == null || section.isBlank()) {

Reply via email to