From 33afb0b4323bb24b627a85f7cd227866054f09fa Mon Sep 17 00:00:00 2001 From: efeire Date: Sun, 20 Sep 2026 12:31:47 +0800 Subject: [PATCH] feat(oceanbase-oracle): split execution time from client wait --- .../java/com/dbx/agent/AbstractJdbcAgent.java | 4 + .../main/java/com/dbx/agent/JdbcExecutor.java | 28 +++- .../java/com/dbx/agent/JsonRpcServer.java | 3 + .../java/com/dbx/agent/QueryPageResult.java | 23 ++- .../main/java/com/dbx/agent/QueryResult.java | 13 +- .../agent/CommonJavaCompatibilityTest.java | 12 ++ .../java/com/dbx/agent/JdbcExecutorTest.java | 8 + .../oceanbaseoracle/OceanBaseOracleAgent.java | 101 +++++++++++ .../OceanBaseOracleAgentTest.java | 108 ++++++++++++ apps/desktop/src/components/grid/DataGrid.vue | 19 ++- .../useResultViewUpdateTiming.spec.ts | 157 ++++++++++++++++++ .../composables/useResultViewUpdateTiming.ts | 59 +++++++ apps/desktop/src/i18n/locales/en.ts | 10 ++ apps/desktop/src/i18n/locales/es.ts | 11 ++ apps/desktop/src/i18n/locales/it.ts | 11 ++ apps/desktop/src/i18n/locales/ja.ts | 10 ++ apps/desktop/src/i18n/locales/ko.ts | 10 ++ apps/desktop/src/i18n/locales/pt-BR.ts | 11 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 9 + apps/desktop/src/i18n/locales/zh-TW.ts | 9 + .../canvasDataGridRendererFrozen.spec.ts | 22 +++ .../lib/__tests__/tabs/tabResultCache.spec.ts | 3 + .../lib/dataGrid/canvasDataGridRenderer.ts | 5 +- apps/desktop/src/lib/tabs/tabResultCache.ts | 8 + .../queryStore.multiStatementError.spec.ts | 30 ++++ .../queryStore.spatialMetadata.spec.ts | 28 ++++ apps/desktop/src/stores/queryStore.ts | 12 ++ apps/desktop/src/types/database.ts | 4 + crates/dbx-cli/src/main.rs | 2 + crates/dbx-core/src/ai/agent_tools.rs | 1 + crates/dbx-core/src/connection/mod.rs | 1 + crates/dbx-core/src/data/table_export.rs | 1 + crates/dbx-core/src/data/transfer.rs | 1 + crates/dbx-core/src/query/mod.rs | 23 +++ crates/dbx-core/src/query/mongo_ops.rs | 1 + crates/dbx-core/src/schema/kingbase.rs | 1 + crates/dbx-core/src/schema/mod.rs | 13 ++ .../table_structure_sql/sqlite_rebuild.rs | 1 + .../dbx-drivers/src/db/clickhouse_driver.rs | 2 + .../dbx-drivers/src/db/cloudflare_d1/mod.rs | 1 + crates/dbx-drivers/src/db/dynamodb_driver.rs | 2 + .../src/db/elasticsearch_driver.rs | 5 + crates/dbx-drivers/src/db/influxdb3_driver.rs | 1 + crates/dbx-drivers/src/db/influxdb_driver.rs | 3 + .../dbx-drivers/src/db/meilisearch_driver.rs | 1 + crates/dbx-drivers/src/db/mysql.rs | 9 + crates/dbx-drivers/src/db/postgres.rs | 5 + crates/dbx-drivers/src/db/rqlite_driver.rs | 2 + crates/dbx-drivers/src/db/sqlite.rs | 2 + crates/dbx-drivers/src/db/sqlite_worker.rs | 1 + crates/dbx-drivers/src/db/sqlserver.rs | 12 ++ crates/dbx-drivers/src/db/turso_driver.rs | 4 + crates/dbx-drivers/src/db/vector_driver.rs | 2 + .../src/db/victoriametrics_driver.rs | 2 + crates/dbx-mcp/src/backend.rs | 1 + crates/dbx-mcp/src/server.rs | 3 + crates/dbx-types/src/types.rs | 22 +++ crates/dbx-web/src/routes/query.rs | 1 + 58 files changed, 843 insertions(+), 11 deletions(-) create mode 100644 apps/desktop/src/composables/__tests__/useResultViewUpdateTiming.spec.ts create mode 100644 apps/desktop/src/composables/useResultViewUpdateTiming.ts diff --git a/agents/common/src/main/java/com/dbx/agent/AbstractJdbcAgent.java b/agents/common/src/main/java/com/dbx/agent/AbstractJdbcAgent.java index 72141f56b0..4412834396 100644 --- a/agents/common/src/main/java/com/dbx/agent/AbstractJdbcAgent.java +++ b/agents/common/src/main/java/com/dbx/agent/AbstractJdbcAgent.java @@ -595,6 +595,10 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent { protected void beforeQueryExecution(Connection connection, int timeoutSecs) throws Exception { } + /** Called for each session RPC before its SQL can replace the connection's last trace ID. */ + protected void beforeAgentMethod(String method, String querySessionId) { + } + protected boolean advancePastUpdateCounts() { return false; } diff --git a/agents/common/src/main/java/com/dbx/agent/JdbcExecutor.java b/agents/common/src/main/java/com/dbx/agent/JdbcExecutor.java index 09ccfebef4..a7eff9772c 100644 --- a/agents/common/src/main/java/com/dbx/agent/JdbcExecutor.java +++ b/agents/common/src/main/java/com/dbx/agent/JdbcExecutor.java @@ -37,6 +37,11 @@ public final class JdbcExecutor { return AgentExecutionContext.jdbcExecutor(); } + public static int statementMaxRows(int maxRows) { + int effectiveMaxRows = Math.max(maxRows, 1); + return effectiveMaxRows == Integer.MAX_VALUE ? Integer.MAX_VALUE : effectiveMaxRows + 1; + } + public QueryResult execute(Connection conn, String sql, String schema, Function setSchemaSql) { return execute(conn, sql, schema, setSchemaSql, DEFAULT_MAX_ROWS, null, this::defaultResultValue); } @@ -141,7 +146,7 @@ public final class JdbcExecutor { activeStatements.add(stmt); try { int effectiveMaxRows = Math.max(maxRows, 1); - stmt.setMaxRows(effectiveMaxRows + 1); + stmt.setMaxRows(statementMaxRows(maxRows)); applyQueryTimeout(stmt, timeoutSecs); if (fetchSize != null && fetchSize > 0) { stmt.setFetchSize(fetchSize); @@ -734,7 +739,7 @@ public final class JdbcExecutor { while (rows.size() < effectivePageSize && session.rowsRead < session.maxRows) { if (!session.resultSet.next()) { closeSession(targetSessions, session.id); - return new QueryPageResult(session.columns, session.columnTypes, rows, 0L, executionTimeMs, false, null, false); + return sessionPageResult(session, rows, executionTimeMs, false, null, false); } rows.add(rowValues(session.resultSet, session.valueReader, session.sqlTypeByIndex, session.typeNameByIndex)); session.rowsRead += 1; @@ -743,21 +748,34 @@ public final class JdbcExecutor { if (session.rowsRead >= session.maxRows) { boolean truncated = session.resultSet.next(); closeSession(targetSessions, session.id); - return new QueryPageResult(session.columns, session.columnTypes, rows, 0L, executionTimeMs, truncated, null, false); + return sessionPageResult(session, rows, executionTimeMs, truncated, null, false); } boolean hasMore = session.resultSet.next(); if (!hasMore) { closeSession(targetSessions, session.id); - return new QueryPageResult(session.columns, session.columnTypes, rows, 0L, executionTimeMs, false, null, false); + return sessionPageResult(session, rows, executionTimeMs, false, null, false); } session.pendingRow = rowValues(session.resultSet, session.valueReader, session.sqlTypeByIndex, session.typeNameByIndex); session.rowsRead += 1; - return new QueryPageResult(session.columns, session.columnTypes, rows, 0L, executionTimeMs, false, session.id, true); + return sessionPageResult(session, rows, executionTimeMs, false, session.id, true); }); } + private static QueryPageResult sessionPageResult( + QuerySession session, + List> rows, + long executionTimeMs, + boolean truncated, + String sessionId, + boolean hasMore + ) { + QueryPageResult result = new QueryPageResult(session.columns, session.columnTypes, rows, 0L, executionTimeMs, truncated, sessionId, hasMore); + result.setCursor_rows_read(session.rowsRead); + return result; + } + private void closeAllSessions(ConcurrentHashMap targetSessions) { List ids = new ArrayList<>(targetSessions.keySet()); for (String id : ids) { diff --git a/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java b/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java index 88b2a2b34c..ee252726b6 100644 --- a/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java +++ b/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java @@ -151,6 +151,9 @@ public final class JsonRpcServer { } private Object dispatch(String method, JsonObject params) throws Exception { + if (agent instanceof AbstractJdbcAgent observingAgent) { + observingAgent.beforeAgentMethod(method, stringOrNull(params, "sessionId")); + } if (AgentProtocol.METHOD_HANDSHAKE.equals(method)) { return AgentProtocol.handshakeResult(); } diff --git a/agents/common/src/main/java/com/dbx/agent/QueryPageResult.java b/agents/common/src/main/java/com/dbx/agent/QueryPageResult.java index ad1c4315fc..bfee05d6ea 100644 --- a/agents/common/src/main/java/com/dbx/agent/QueryPageResult.java +++ b/agents/common/src/main/java/com/dbx/agent/QueryPageResult.java @@ -12,6 +12,9 @@ public final class QueryPageResult { private List> rows; private long affected_rows; private long execution_time_ms; + private Long server_execute_time_us; + /** Number of rows read by this JDBC cursor across all pages; never sent over JSON RPC. */ + private transient long cursor_rows_read; private boolean truncated; private String session_id; private boolean has_more; @@ -97,6 +100,14 @@ public final class QueryPageResult { return execution_time_ms; } + public Long getServer_execute_time_us() { + return server_execute_time_us; + } + + public long getCursor_rows_read() { + return cursor_rows_read; + } + public boolean getTruncated() { return truncated; } @@ -140,6 +151,14 @@ public final class QueryPageResult { this.execution_time_ms = execution_time_ms; } + public void setServer_execute_time_us(Long server_execute_time_us) { + this.server_execute_time_us = server_execute_time_us; + } + + public void setCursor_rows_read(long cursor_rows_read) { + this.cursor_rows_read = cursor_rows_read; + } + public void setTruncated(boolean truncated) { this.truncated = truncated; } @@ -159,6 +178,7 @@ public final class QueryPageResult { QueryPageResult that = (QueryPageResult) other; return affected_rows == that.affected_rows && execution_time_ms == that.execution_time_ms + && Objects.equals(server_execute_time_us, that.server_execute_time_us) && truncated == that.truncated && has_more == that.has_more && Objects.equals(columns, that.columns) @@ -171,7 +191,7 @@ public final class QueryPageResult { @Override public int hashCode() { - return Objects.hash(columns, column_types, spatial_columns, spatial_values, rows, affected_rows, execution_time_ms, truncated, session_id, has_more); + return Objects.hash(columns, column_types, spatial_columns, spatial_values, rows, affected_rows, execution_time_ms, server_execute_time_us, truncated, session_id, has_more); } @Override @@ -183,6 +203,7 @@ public final class QueryPageResult { + ", rows=" + rows + ", affected_rows=" + affected_rows + ", execution_time_ms=" + execution_time_ms + + ", server_execute_time_us=" + server_execute_time_us + ", truncated=" + truncated + ", session_id=" + session_id + ", has_more=" + has_more diff --git a/agents/common/src/main/java/com/dbx/agent/QueryResult.java b/agents/common/src/main/java/com/dbx/agent/QueryResult.java index 8b61b29f66..e45fdb91b5 100644 --- a/agents/common/src/main/java/com/dbx/agent/QueryResult.java +++ b/agents/common/src/main/java/com/dbx/agent/QueryResult.java @@ -17,6 +17,7 @@ public final class QueryResult { private List> rows; private long affected_rows; private long execution_time_ms; + private Long server_execute_time_us; private boolean truncated; public QueryResult() { @@ -84,6 +85,10 @@ public final class QueryResult { return execution_time_ms; } + public Long getServer_execute_time_us() { + return server_execute_time_us; + } + public boolean getTruncated() { return truncated; } @@ -119,6 +124,10 @@ public final class QueryResult { this.execution_time_ms = execution_time_ms; } + public void setServer_execute_time_us(Long server_execute_time_us) { + this.server_execute_time_us = server_execute_time_us; + } + public void setTruncated(boolean truncated) { this.truncated = truncated; } @@ -190,6 +199,7 @@ public final class QueryResult { QueryResult that = (QueryResult) other; return affected_rows == that.affected_rows && execution_time_ms == that.execution_time_ms + && Objects.equals(server_execute_time_us, that.server_execute_time_us) && truncated == that.truncated && Objects.equals(columns, that.columns) && Objects.equals(column_types, that.column_types) @@ -200,7 +210,7 @@ public final class QueryResult { @Override public int hashCode() { - return Objects.hash(columns, column_types, spatial_columns, spatial_values, rows, affected_rows, execution_time_ms, truncated); + return Objects.hash(columns, column_types, spatial_columns, spatial_values, rows, affected_rows, execution_time_ms, server_execute_time_us, truncated); } @Override @@ -212,6 +222,7 @@ public final class QueryResult { + ", rows=" + rows + ", affected_rows=" + affected_rows + ", execution_time_ms=" + execution_time_ms + + ", server_execute_time_us=" + server_execute_time_us + ", truncated=" + truncated + ")"; } diff --git a/agents/common/src/test/java/com/dbx/agent/CommonJavaCompatibilityTest.java b/agents/common/src/test/java/com/dbx/agent/CommonJavaCompatibilityTest.java index ea9a24058e..fcb31e5a20 100644 --- a/agents/common/src/test/java/com/dbx/agent/CommonJavaCompatibilityTest.java +++ b/agents/common/src/test/java/com/dbx/agent/CommonJavaCompatibilityTest.java @@ -1,6 +1,7 @@ package com.dbx.agent; import com.google.gson.JsonArray; +import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.junit.jupiter.api.Test; @@ -34,6 +35,17 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class CommonJavaCompatibilityTest { + @Test + void emitsOptionalServerAuditTimeWithoutLeakingCursorBookkeeping() { + Gson gson = new Gson(); + QueryPageResult page = new QueryPageResult(); + assertFalse(gson.toJsonTree(page).getAsJsonObject().has("server_execute_time_us")); + page.setServer_execute_time_us(370L); + page.setCursor_rows_read(500); + assertEquals(370L, gson.toJsonTree(page).getAsJsonObject().get("server_execute_time_us").getAsLong()); + assertFalse(gson.toJsonTree(page).getAsJsonObject().has("cursor_rows_read")); + } + @Test void definesSharedAgentProtocolContract() { assertEquals("handshake", AgentProtocol.METHOD_HANDSHAKE); diff --git a/agents/common/src/test/java/com/dbx/agent/JdbcExecutorTest.java b/agents/common/src/test/java/com/dbx/agent/JdbcExecutorTest.java index 6f19c26393..11637f24bf 100644 --- a/agents/common/src/test/java/com/dbx/agent/JdbcExecutorTest.java +++ b/agents/common/src/test/java/com/dbx/agent/JdbcExecutorTest.java @@ -26,6 +26,14 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class JdbcExecutorTest { + @Test + void statementRowLimitIncludesOverflowProbeWithoutIntegerOverflow() { + assertEquals(2, JdbcExecutor.statementMaxRows(0)); + assertEquals(JdbcExecutor.DEFAULT_MAX_ROWS + 1, + JdbcExecutor.statementMaxRows(JdbcExecutor.DEFAULT_MAX_ROWS)); + assertEquals(Integer.MAX_VALUE, JdbcExecutor.statementMaxRows(Integer.MAX_VALUE)); + } + @Test void blobResultValuesPreferBlobObjectsWhenGetBytesFails() throws Exception { ResultSet rs = resultSet( diff --git a/agents/drivers/oceanbase-oracle/src/main/java/com/dbx/agent/oceanbaseoracle/OceanBaseOracleAgent.java b/agents/drivers/oceanbase-oracle/src/main/java/com/dbx/agent/oceanbaseoracle/OceanBaseOracleAgent.java index d5dbcedf8a..08b090cd0d 100644 --- a/agents/drivers/oceanbase-oracle/src/main/java/com/dbx/agent/oceanbaseoracle/OceanBaseOracleAgent.java +++ b/agents/drivers/oceanbase-oracle/src/main/java/com/dbx/agent/oceanbaseoracle/OceanBaseOracleAgent.java @@ -1,10 +1,12 @@ package com.dbx.agent.oceanbaseoracle; import com.dbx.agent.ColumnInfo; +import com.dbx.agent.AgentProtocol; import com.dbx.agent.ConfiguredJdbcAgent; import com.dbx.agent.ConnectParams; import com.dbx.agent.DatabaseInfo; import com.dbx.agent.DdlBuilder; +import com.dbx.agent.ExecuteQueryOptions; import com.dbx.agent.ForeignKeyInfo; import com.dbx.agent.IndexInfo; import com.dbx.agent.JdbcAgentProfile; @@ -16,12 +18,17 @@ import com.dbx.agent.ObjectInfo; import com.dbx.agent.ObjectSource; import com.dbx.agent.OracleObjectPrivilege; import com.dbx.agent.PartitionInfo; +import com.dbx.agent.QueryPageOptions; +import com.dbx.agent.QueryPageResult; +import com.dbx.agent.QueryResult; import com.dbx.agent.TableInfo; import com.dbx.agent.TriggerInfo; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.sql.Types; import java.util.ArrayDeque; import java.util.ArrayList; @@ -51,6 +58,9 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent { "GGSYS", "FLOWS_FILES", "APEX_PUBLIC_USER", "GSMROOTUSER", "SYSRAC" ); private boolean queryTimeoutChanged; + private String auditEligibleCursorId; + private static final String SQL_AUDIT_BY_TRACE = "SELECT EXECUTE_TIME, RETURN_ROWS FROM SYS.GV$OB_SQL_AUDIT " + + "WHERE TRACE_ID = ? AND IS_INNER_SQL = 0 AND IS_EXECUTOR_RPC = 0 AND ROWNUM <= 2"; public static final JdbcAgentProfile OCEANBASE_ORACLE_PROFILE = new JdbcAgentProfile( "com.oceanbase.jdbc.Driver", @@ -70,6 +80,97 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent { super(OCEANBASE_ORACLE_PROFILE); } + @Override + public QueryResult executeQuery(String sql, String schema, ExecuteQueryOptions options) { + auditEligibleCursorId = null; + QueryResult result = super.executeQuery(sql, schema, options); + if (!result.getTruncated() && !result.getColumns().isEmpty()) { + result.setServer_execute_time_us(availableServerExecuteTimeUs( + result.getRows().size(), JdbcExecutor.statementMaxRows(options.getMaxRows()) + )); + } + return result; + } + + @Override + public QueryPageResult executeQueryPage(String sql, String schema, QueryPageOptions options) { + auditEligibleCursorId = null; + QueryPageResult result = super.executeQueryPage(sql, schema, options); + if (result.getHas_more()) { + auditEligibleCursorId = result.getSession_id(); + return result; + } + return withCompletedCursorServerTiming(result); + } + + @Override + public QueryPageResult fetchQueryPage(String sessionId, int pageSize) { + boolean auditEligible = sessionId != null && sessionId.equals(auditEligibleCursorId); + QueryPageResult result = super.fetchQueryPage(sessionId, pageSize); + if (!result.getHas_more()) auditEligibleCursorId = null; + return auditEligible ? withCompletedCursorServerTiming(result) : result; + } + + @Override + protected void beforeAgentMethod(String method, String querySessionId) { + if (!AgentProtocol.METHOD_FETCH_QUERY_PAGE.equals(method) + || auditEligibleCursorId == null + || !auditEligibleCursorId.equals(querySessionId)) { + auditEligibleCursorId = null; + } + } + + private QueryPageResult withCompletedCursorServerTiming(QueryPageResult result) { + // JdbcExecutor closes the ResultSet and Statement before returning a terminal + // page. Never issue audit SQL while Connector/J still owns an open cursor. + if (!result.getHas_more() && !result.getTruncated() && !result.getColumns().isEmpty()) { + result.setServer_execute_time_us(availableServerExecuteTimeUs(result.getCursor_rows_read(), 0)); + } + return result; + } + + private Long availableServerExecuteTimeUs(long expectedRows, int statementMaxRows) { + try { + return serverExecuteTimeUs(requireConnected(), expectedRows, statementMaxRows); + } catch (RuntimeException ignored) { + return null; + } + } + + static Long serverExecuteTimeUs(Connection connection, long expectedRows, int statementMaxRows) { + try { + String traceId; + try (Statement statement = connection.createStatement()) { + // Connector/J resets the session's SQL_SELECT_LIMIT in execute prolog + // when this differs from the previous statement. That SET would + // replace the query's LAST_TRACE_ID before we can read it. + statement.setMaxRows(statementMaxRows); + statement.setQueryTimeout(1); + try (ResultSet traces = statement.executeQuery("SELECT LAST_TRACE_ID() FROM DUAL")) { + traceId = traces.next() ? traces.getString(1) : null; + } + } + if (traceId == null || traceId.isBlank()) return null; + try (PreparedStatement statement = connection.prepareStatement(SQL_AUDIT_BY_TRACE)) { + statement.setMaxRows(statementMaxRows); + statement.setQueryTimeout(1); + statement.setString(1, traceId); + try (ResultSet audit = statement.executeQuery()) { + if (!audit.next()) return null; + long executionUs = audit.getLong(1); + if (audit.wasNull() || executionUs < 0) return null; + long returnedRows = audit.getLong(2); + if (audit.wasNull() || returnedRows != expectedRows || audit.next()) return null; + return executionUs; + } + } + } catch (SQLException | RuntimeException ignored) { + // SQL Audit can be disabled, unavailable to this account, or evicted. + // The user's query result remains valid; no wall-clock substitute is used. + return null; + } + } + @Override protected String buildJdbcUrl(ConnectParams params) { return buildUrl(params); diff --git a/agents/drivers/oceanbase-oracle/src/test/java/com/dbx/agent/oceanbaseoracle/OceanBaseOracleAgentTest.java b/agents/drivers/oceanbase-oracle/src/test/java/com/dbx/agent/oceanbaseoracle/OceanBaseOracleAgentTest.java index 701706a80d..9eaed4693e 100644 --- a/agents/drivers/oceanbase-oracle/src/test/java/com/dbx/agent/oceanbaseoracle/OceanBaseOracleAgentTest.java +++ b/agents/drivers/oceanbase-oracle/src/test/java/com/dbx/agent/oceanbaseoracle/OceanBaseOracleAgentTest.java @@ -1,5 +1,6 @@ package com.dbx.agent.oceanbaseoracle; +import com.dbx.agent.AgentProtocol; import com.dbx.agent.ColumnInfo; import com.dbx.agent.ConnectParams; import com.dbx.agent.ExecuteQueryOptions; @@ -7,6 +8,7 @@ import com.dbx.agent.MetadataListConstraints; import com.dbx.agent.ObjectInfo; import com.dbx.agent.ObjectSource; import com.dbx.agent.QueryPageOptions; +import com.dbx.agent.QueryPageResult; import com.dbx.agent.QueryResult; import com.dbx.agent.TableInfo; import com.dbx.agent.test.TestSupport; @@ -133,6 +135,68 @@ class OceanBaseOracleAgentTest { Assertions.assertThrows(IllegalArgumentException.class, () -> OceanBaseOracleAgent.queryTimeoutSql(-1)); } + @Test + void readsServerExecutionOnlyFromOneMatchingAuditRow() { + List statements = new ArrayList<>(); + List parameters = new ArrayList<>(); + List maxRows = new ArrayList<>(); + Assertions.assertEquals(370L, OceanBaseOracleAgent.serverExecuteTimeUs( + auditTimingConnection(1, 500, false, statements, parameters, maxRows), 500, 1001 + )); + Assertions.assertEquals(List.of(1001, 1001), maxRows, + "trace and audit statements must preserve the target statement's session row limit"); + Assertions.assertEquals(List.of("trace-1"), parameters); + Assertions.assertTrue(statements.get(1).contains("IS_INNER_SQL = 0 AND IS_EXECUTOR_RPC = 0")); + Assertions.assertEquals(null, OceanBaseOracleAgent.serverExecuteTimeUs(auditTimingConnection(0, 500, false, new ArrayList<>(), new ArrayList<>(), new ArrayList<>()), 500, 0)); + Assertions.assertEquals(null, OceanBaseOracleAgent.serverExecuteTimeUs(auditTimingConnection(2, 500, false, new ArrayList<>(), new ArrayList<>(), new ArrayList<>()), 500, 0)); + Assertions.assertEquals(null, OceanBaseOracleAgent.serverExecuteTimeUs(auditTimingConnection(1, 1, false, new ArrayList<>(), new ArrayList<>(), new ArrayList<>()), 500, 0)); + Assertions.assertEquals(null, OceanBaseOracleAgent.serverExecuteTimeUs(auditTimingConnection(1, 500, true, new ArrayList<>(), new ArrayList<>(), new ArrayList<>()), 500, 0)); + } + + + @Test + void doesNotAuditAnOpenCursorAfterAnotherSessionMethodCanReplaceItsTrace() { + int[] row = {-1}; + ResultSetMetaData meta = proxy(ResultSetMetaData.class, (method, args) -> { + if ("getColumnCount".equals(method.getName())) return 1; + if ("getColumnLabel".equals(method.getName())) return "N"; + if ("getColumnType".equals(method.getName())) return Types.INTEGER; + if ("getColumnTypeName".equals(method.getName())) return "NUMBER"; + return defaultValue(method.getReturnType()); + }); + ResultSet cursor = proxy(ResultSet.class, (method, args) -> { + if ("next".equals(method.getName())) return ++row[0] < 2; + if ("getMetaData".equals(method.getName())) return meta; + if ("getObject".equals(method.getName()) || "getInt".equals(method.getName())) return row[0] + 1; + return defaultValue(method.getReturnType()); + }); + int[] statementsCreated = {0}; + Statement statement = proxy(Statement.class, (method, args) -> { + if ("execute".equals(method.getName())) return !String.valueOf(args[0]).startsWith("ALTER SESSION"); + if ("getResultSet".equals(method.getName())) return cursor; + return defaultValue(method.getReturnType()); + }); + Connection connection = proxy(Connection.class, (method, args) -> { + if ("createStatement".equals(method.getName())) { + statementsCreated[0]++; + return statement; + } + if ("isClosed".equals(method.getName())) return false; + return defaultValue(method.getReturnType()); + }); + OceanBaseOracleAgent agent = new OceanBaseOracleAgent(); + TestSupport.setPrivateConnection(agent, connection); + QueryPageResult first = agent.executeQueryPage("SELECT N FROM T", null, new QueryPageOptions(1, null, 10, 5)); + Assertions.assertTrue(first.getHas_more()); + Assertions.assertEquals(2, statementsCreated[0]); + + agent.beforeAgentMethod(AgentProtocol.METHOD_LIST_TABLES, null); + QueryPageResult last = agent.fetchQueryPage(first.getSession_id(), 1); + Assertions.assertFalse(last.getHas_more()); + Assertions.assertNull(last.getServer_execute_time_us()); + Assertions.assertEquals(2, statementsCreated[0], "no LAST_TRACE_ID query may run for an invalidated cursor"); + } + @Test void treatsZeroQueryTimeoutAsUnlimitedForOceanBaseSession() { List sql = new ArrayList<>(); @@ -1198,6 +1262,50 @@ class OceanBaseOracleAgentTest { }); } + private static Connection auditTimingConnection(int auditRows, long returnedRows, boolean denied, + List sql, List parameters, List maxRows) { + ResultSet trace = resultSet(new String[]{"TRACE_ID"}, new Object[][]{{"trace-1"}}); + int[] auditIndex = {-1}; + ResultSet audit = proxy(ResultSet.class, (method, args) -> { + switch (method.getName()) { + case "next": + auditIndex[0]++; + return auditIndex[0] < auditRows; + case "getLong": + return ((Number) args[0]).intValue() == 1 ? 370L : returnedRows; + default: + return defaultValue(method.getReturnType()); + } + }); + Statement traceStatement = proxy(Statement.class, (method, args) -> { + if ("setQueryTimeout".equals(method.getName())) Assertions.assertEquals(1, args[0]); + if ("setMaxRows".equals(method.getName())) maxRows.add((Integer) args[0]); + if ("executeQuery".equals(method.getName())) { + sql.add(String.valueOf(args[0])); + return trace; + } + return defaultValue(method.getReturnType()); + }); + PreparedStatement auditStatement = proxy(PreparedStatement.class, (method, args) -> { + if ("setQueryTimeout".equals(method.getName())) Assertions.assertEquals(1, args[0]); + if ("setMaxRows".equals(method.getName())) maxRows.add((Integer) args[0]); + if ("setString".equals(method.getName())) parameters.add(String.valueOf(args[1])); + if ("executeQuery".equals(method.getName())) { + if (denied) throw new SQLException("audit access denied", "42000", 1044); + return audit; + } + return defaultValue(method.getReturnType()); + }); + return proxy(Connection.class, (method, args) -> { + if ("createStatement".equals(method.getName())) return traceStatement; + if ("prepareStatement".equals(method.getName())) { + sql.add(String.valueOf(args[0])); + return auditStatement; + } + return defaultValue(method.getReturnType()); + }); + } + private static ResultSet blobResultSet() { String[] columns = {"PAYLOAD", "EMPTY_PAYLOAD", "DESCRIPTION"}; int[] sqlTypes = {Types.BLOB, Types.BLOB, Types.VARCHAR}; diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index be120b9379..036e9c350b 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -3,6 +3,7 @@ import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage"; import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue"; import { useUpdateBlocker } from "@/lib/app/updatePreparation"; +import { useResultViewUpdateTiming } from "@/composables/useResultViewUpdateTiming"; import { computed, nextTick, onMounted, onUnmounted, onActivated, onDeactivated, ref, shallowRef, toRaw, useSlots, watch, defineAsyncComponent, type Component, type CSSProperties } from "vue"; import { useI18n } from "vue-i18n"; import { @@ -6532,6 +6533,11 @@ let canvasPixelRatioMediaQuery: MediaQueryList | null = null; let canvasPixelRatioMediaQueryCleanup: (() => void) | null = null; let dataGridIsActive = true; let canvasRuntime: DataGridCanvasRuntime; +const { elapsedMs: resultViewUpdateMs, canvasDrawCompleted: completeResultCanvasDraw } = useResultViewUpdateTiming( + () => props.result, + () => resolvedDatabaseType.value === "oceanbase-oracle" && isResultsContext.value, + () => (useCanvasGridRows.value ? "canvas" : "dom"), +); watch( () => [totalWidth.value, displayRowCount.value, useCanvasGridRows.value, props.result.columns.join("\u0000")], @@ -7228,7 +7234,8 @@ function drawCanvasGrid() { const scroller = canvasScrollerElement(); if (!canvas || !scroller || !useCanvasGridRows.value) return; - drawCanvasDataGrid({ + const drawnResult = props.result; + const drawn = drawCanvasDataGrid({ canvas, scroller, width: Math.max(1, canvasSurfaceWidth.value || scroller.clientWidth), @@ -7271,7 +7278,9 @@ function drawCanvasGrid() { flatteningMultiLineEnabled: flatteningMultiLineEnabled.value, showWhitespace: showWhitespaceEnabled.value, }); + if (!drawn) return; flipCanvasSurface(); + completeResultCanvasDraw(drawnResult); } function flipCanvasSurface() { @@ -13658,7 +13667,13 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val (truncated) {{ t("grid.rowsAffected", { count: result.affected_rows }) }} - {{ result.execution_time_ms }}ms + + {{ result.execution_time_ms }}ms