mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(oceanbase-oracle): return dbms output text
This commit is contained in:
@@ -202,7 +202,7 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
|
||||
options.getFetchSize(),
|
||||
options.getTimeoutSecs(),
|
||||
resultValueReader(),
|
||||
JdbcExecutor.StatementMessageReader.NONE,
|
||||
statementMessageReader(),
|
||||
advancePastUpdateCounts()
|
||||
);
|
||||
}
|
||||
@@ -221,6 +221,7 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
|
||||
this::resetSchemaSQL,
|
||||
options,
|
||||
resultValueReader(),
|
||||
statementMessageReader(),
|
||||
advancePastUpdateCounts()
|
||||
);
|
||||
}
|
||||
@@ -602,6 +603,10 @@ public abstract class AbstractJdbcAgent extends BaseDatabaseAgent {
|
||||
protected void beforeQueryExecution(Connection connection, int timeoutSecs) throws Exception {
|
||||
}
|
||||
|
||||
protected JdbcExecutor.StatementMessageReader statementMessageReader() {
|
||||
return JdbcExecutor.StatementMessageReader.NONE;
|
||||
}
|
||||
|
||||
/** Called for each session RPC before its SQL can replace the connection's last trace ID. */
|
||||
protected void beforeAgentMethod(String method, String querySessionId) {
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -300,6 +301,7 @@ public final class JdbcExecutor {
|
||||
try (ResultSet rs = stmt.getResultSet()) {
|
||||
result = readResultSet(rs, elapsed, effectiveMaxRows, valueReader);
|
||||
}
|
||||
appendStatementReaderMessages(result, stmt, statementMessageReader);
|
||||
// `elapsed` stops at `stmt.execute()`. Retrieving and
|
||||
// converting the rows above is frequently the dominant
|
||||
// cost of a statement, so report the whole duration the
|
||||
@@ -316,7 +318,9 @@ public final class JdbcExecutor {
|
||||
false
|
||||
);
|
||||
}
|
||||
return withStatementMessages(result, stmt, effectiveMaxRows, statementMessageReader);
|
||||
QueryResult completed = withStatementMessages(result, stmt, effectiveMaxRows, statementMessageReader);
|
||||
completed.setExecution_time_ms(System.currentTimeMillis() - start);
|
||||
return completed;
|
||||
} finally {
|
||||
activeStatements.remove(stmt);
|
||||
}
|
||||
@@ -411,6 +415,21 @@ public final class JdbcExecutor {
|
||||
return executePage(conn, sql, schema, setSchemaSql, () -> "", options, valueReader, statementMessageReader, sessions);
|
||||
}
|
||||
|
||||
public QueryPageResult executePage(
|
||||
Connection conn,
|
||||
String sql,
|
||||
String schema,
|
||||
Function<String, String> setSchemaSql,
|
||||
Supplier<String> resetSchemaSql,
|
||||
QueryPageOptions options,
|
||||
ResultValueReader valueReader,
|
||||
StatementMessageReader statementMessageReader,
|
||||
boolean advancePastUpdateCounts
|
||||
) {
|
||||
return executePage(conn, sql, schema, setSchemaSql, resetSchemaSql, options, valueReader,
|
||||
statementMessageReader, sessions, advancePastUpdateCounts);
|
||||
}
|
||||
|
||||
public QueryPageResult executePage(
|
||||
Connection conn,
|
||||
String sql,
|
||||
@@ -471,6 +490,20 @@ public final class JdbcExecutor {
|
||||
StatementMessageReader.NONE, sessions, false, true);
|
||||
}
|
||||
|
||||
public QueryPageResult executeBoundedPage(
|
||||
Connection conn,
|
||||
String sql,
|
||||
String schema,
|
||||
Function<String, String> setSchemaSql,
|
||||
Supplier<String> resetSchemaSql,
|
||||
QueryPageOptions options,
|
||||
ResultValueReader valueReader,
|
||||
StatementMessageReader statementMessageReader
|
||||
) {
|
||||
return executePage(conn, sql, schema, setSchemaSql, resetSchemaSql, options, valueReader,
|
||||
statementMessageReader, sessions, false, true);
|
||||
}
|
||||
|
||||
public QueryPageResult startTableRead(
|
||||
Connection conn,
|
||||
String sql,
|
||||
@@ -600,6 +633,7 @@ public final class JdbcExecutor {
|
||||
} else {
|
||||
appendStatementWarnings(result, stmt);
|
||||
}
|
||||
result.setExecution_time_ms(System.currentTimeMillis() - start);
|
||||
activeStatements.remove(stmt);
|
||||
stmt.close();
|
||||
QueryPageResult page = new QueryPageResult(
|
||||
@@ -642,7 +676,8 @@ public final class JdbcExecutor {
|
||||
sqlTypeByIndex,
|
||||
typeNameByIndex,
|
||||
Math.max(options.getMaxRows(), 1),
|
||||
valueReader
|
||||
valueReader,
|
||||
statementMessageReader
|
||||
);
|
||||
createdSession = session;
|
||||
targetSessions.put(sessionId, session);
|
||||
@@ -939,7 +974,7 @@ public final class JdbcExecutor {
|
||||
while (rows.size() < effectivePageSize && session.rowsRead < session.maxRows) {
|
||||
if (!session.resultSet.next()) {
|
||||
QueryPageResult page = sessionPageResult(session, rows, executionTimeMs, false, null, false);
|
||||
closeSession(targetSessions, session.id);
|
||||
appendStatementReaderMessages(page, closeSessionMessages(targetSessions, session.id));
|
||||
return page;
|
||||
}
|
||||
rows.add(rowValues(session.resultSet, session.valueReader, session.sqlTypeByIndex, session.typeNameByIndex));
|
||||
@@ -949,14 +984,14 @@ public final class JdbcExecutor {
|
||||
if (session.rowsRead >= session.maxRows) {
|
||||
boolean truncated = session.resultSet.next();
|
||||
QueryPageResult page = sessionPageResult(session, rows, executionTimeMs, truncated, null, false);
|
||||
closeSession(targetSessions, session.id);
|
||||
appendStatementReaderMessages(page, closeSessionMessages(targetSessions, session.id));
|
||||
return page;
|
||||
}
|
||||
|
||||
boolean hasMore = session.resultSet.next();
|
||||
if (!hasMore) {
|
||||
QueryPageResult page = sessionPageResult(session, rows, executionTimeMs, false, null, false);
|
||||
closeSession(targetSessions, session.id);
|
||||
appendStatementReaderMessages(page, closeSessionMessages(targetSessions, session.id));
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -990,9 +1025,13 @@ public final class JdbcExecutor {
|
||||
}
|
||||
|
||||
private boolean closeSession(ConcurrentHashMap<String, QuerySession> targetSessions, String sessionId) {
|
||||
return closeSessionMessages(targetSessions, sessionId) != null;
|
||||
}
|
||||
|
||||
private List<String> closeSessionMessages(ConcurrentHashMap<String, QuerySession> targetSessions, String sessionId) {
|
||||
QuerySession session = targetSessions.remove(sessionId);
|
||||
if (session == null) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
synchronized (session) {
|
||||
activeStatements.remove(session.statement);
|
||||
@@ -1000,12 +1039,17 @@ public final class JdbcExecutor {
|
||||
session.resultSet.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
List<String> messages = Collections.emptyList();
|
||||
try {
|
||||
messages = session.statementMessageReader.close(session.statement);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
try {
|
||||
session.statement.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return messages == null ? Collections.emptyList() : messages;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static String safeColumnTypeName(ResultSetMetaData meta, int columnIndex) {
|
||||
@@ -1144,6 +1188,32 @@ public final class JdbcExecutor {
|
||||
return messageResult;
|
||||
}
|
||||
|
||||
private static void appendStatementReaderMessages(
|
||||
QueryResult result,
|
||||
Statement statement,
|
||||
StatementMessageReader reader
|
||||
) {
|
||||
try {
|
||||
List<String> messages = reader.close(statement);
|
||||
if (messages != null) {
|
||||
for (String message : messages) {
|
||||
if (message != null) result.addInformationalMessage(message, null);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// Driver-specific informational output is advisory.
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendStatementReaderMessages(QueryPageResult page, List<String> messages) {
|
||||
if (messages == null || messages.isEmpty()) return;
|
||||
List<Map<String, Object>> result = new ArrayList<>(page.getMessages());
|
||||
for (String message : messages) {
|
||||
if (message != null) result.add(Map.of("severity", "INFO", "message", message));
|
||||
}
|
||||
page.setMessages(result);
|
||||
}
|
||||
|
||||
private QueryResult emptyQueryResult(long start) {
|
||||
return new QueryResult(
|
||||
Collections.emptyList(),
|
||||
@@ -1217,6 +1287,10 @@ public final class JdbcExecutor {
|
||||
StatementMessageReader NONE = statement -> Collections.emptyList();
|
||||
|
||||
List<String> read(Statement statement) throws SQLException;
|
||||
|
||||
default List<String> close(Statement statement) throws SQLException {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1262,6 +1336,7 @@ public final class JdbcExecutor {
|
||||
private final String[] typeNameByIndex;
|
||||
private final int maxRows;
|
||||
private final ResultValueReader valueReader;
|
||||
private final StatementMessageReader statementMessageReader;
|
||||
private int rowsRead;
|
||||
private List<Object> pendingRow;
|
||||
private long lastAccessedAtMillis;
|
||||
@@ -1275,7 +1350,8 @@ public final class JdbcExecutor {
|
||||
int[] sqlTypeByIndex,
|
||||
String[] typeNameByIndex,
|
||||
int maxRows,
|
||||
ResultValueReader valueReader
|
||||
ResultValueReader valueReader,
|
||||
StatementMessageReader statementMessageReader
|
||||
) {
|
||||
this.id = id;
|
||||
this.statement = statement;
|
||||
@@ -1286,6 +1362,7 @@ public final class JdbcExecutor {
|
||||
this.typeNameByIndex = typeNameByIndex;
|
||||
this.maxRows = maxRows;
|
||||
this.valueReader = valueReader;
|
||||
this.statementMessageReader = statementMessageReader;
|
||||
this.lastAccessedAtMillis = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,39 @@ class JdbcExecutorTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeCountsDriverMessageReadingInExecutionTime() {
|
||||
JdbcExecutor.StatementMessageReader reader = statement -> {
|
||||
try {
|
||||
Thread.sleep(30L);
|
||||
} catch (InterruptedException error) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new SQLException(error);
|
||||
}
|
||||
return Collections.singletonList("procedure output");
|
||||
};
|
||||
|
||||
QueryResult result = JdbcExecutor.INSTANCE.execute(
|
||||
executionConnection(false, -1, null, new AtomicInteger(), null, null),
|
||||
"BEGIN OUTPUT_TEXT(); END;", "", schema -> "", () -> "", 100, null, 0,
|
||||
JdbcExecutor.INSTANCE::defaultResultValue, reader
|
||||
);
|
||||
|
||||
assertTrue(result.getExecution_time_ms() >= 25L,
|
||||
"driver message reading must count toward execution_time_ms but was " + result.getExecution_time_ms() + "ms");
|
||||
for (boolean bounded : List.of(false, true)) {
|
||||
Connection connection = executionConnection(false, -1, null, new AtomicInteger(), null, null);
|
||||
QueryPageResult page = bounded
|
||||
? JdbcExecutor.INSTANCE.executeBoundedPage(connection, "BEGIN OUTPUT_TEXT(); END;", "",
|
||||
schema -> "", () -> "", new QueryPageOptions(), JdbcExecutor.INSTANCE::defaultResultValue, reader)
|
||||
: JdbcExecutor.INSTANCE.executePage(connection, "BEGIN OUTPUT_TEXT(); END;", "",
|
||||
schema -> "", () -> "", new QueryPageOptions(), JdbcExecutor.INSTANCE::defaultResultValue, reader, false);
|
||||
assertEquals(List.of(List.of("procedure output")), page.getRows());
|
||||
assertTrue(page.getExecution_time_ms() >= 25L,
|
||||
"paged message reading must count toward execution_time_ms but was " + page.getExecution_time_ms() + "ms");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeReturnsMultipleStatementWarningsForNoResultStatements() {
|
||||
SQLWarning first = new SQLWarning("identity value is 443", "S0003", 7998);
|
||||
@@ -336,6 +369,42 @@ class JdbcExecutorTest {
|
||||
assertEquals("informational", result.getMessages().get(0).get("message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeRetainsRowsAndAddsDriverMessagesWhenResultSetCompletes() {
|
||||
CountingResultSetFixture fixture = countingResultSet(new Object[][]{{1, "Ada"}});
|
||||
boolean[] closed = {false};
|
||||
JdbcExecutor.StatementMessageReader reader = new JdbcExecutor.StatementMessageReader() {
|
||||
@Override
|
||||
public List<String> read(Statement statement) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> close(Statement statement) {
|
||||
closed[0] = true;
|
||||
return Arrays.asList("procedure output");
|
||||
}
|
||||
};
|
||||
|
||||
QueryResult result = JdbcExecutor.INSTANCE.execute(
|
||||
executionConnection(true, -1, null, new AtomicInteger(), fixture.resultSet(), null),
|
||||
"BEGIN OPEN_RESULT(); END;",
|
||||
"",
|
||||
schema -> "",
|
||||
() -> "",
|
||||
100,
|
||||
null,
|
||||
0,
|
||||
JdbcExecutor.INSTANCE::defaultResultValue,
|
||||
reader
|
||||
);
|
||||
|
||||
assertTrue(closed[0]);
|
||||
assertEquals(Arrays.asList("id", "name"), result.getColumns());
|
||||
assertEquals(Arrays.asList(Arrays.asList(1, "Ada")), result.getRows());
|
||||
assertEquals("procedure output", result.getMessages().get(0).get("message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executePageRetainsWarningsWithOrdinaryRows() {
|
||||
CountingResultSetFixture fixture = countingResultSet(new Object[][]{{1, "Ada"}});
|
||||
|
||||
+186
-6
@@ -35,10 +35,13 @@ import com.dbx.agent.SpatialColumn;
|
||||
import com.dbx.agent.TableInfo;
|
||||
import com.dbx.agent.TriggerInfo;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.SQLTimeoutException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
@@ -73,6 +76,9 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent {
|
||||
private boolean queryTimeoutChanged;
|
||||
private final OceanBaseLobValues lobValues = new OceanBaseLobValues();
|
||||
private boolean deferCharacterLobs;
|
||||
private boolean captureDbmsOutput;
|
||||
private boolean dbmsOutputEnabled;
|
||||
private String dbmsOutputWarning;
|
||||
|
||||
public static final JdbcAgentProfile OCEANBASE_ORACLE_PROFILE = new JdbcAgentProfile(
|
||||
"com.oceanbase.jdbc.Driver",
|
||||
@@ -108,8 +114,11 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent {
|
||||
@Override
|
||||
public QueryResult executeQuery(String sql, String schema, ExecuteQueryOptions options) {
|
||||
var lobCheckpoint = lobValues.checkpoint();
|
||||
Connection outputConnection = null;
|
||||
try (QueryTiming timing = QueryTiming.begin()) {
|
||||
prepareLobExecution(sql, options.getDeferLobs());
|
||||
captureDbmsOutput = isDbmsOutputStatement(sql);
|
||||
outputConnection = requireConnected();
|
||||
QueryResult result = super.executeQuery(sql, schema, options);
|
||||
var marked = OceanBaseLobValues.mark(result.getColumns(), result.getColumn_types(), result.getRows());
|
||||
if (marked != null) {
|
||||
@@ -124,9 +133,13 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent {
|
||||
result.setQuery_timings_ms(timing.finish());
|
||||
return result;
|
||||
} catch (RuntimeException error) {
|
||||
if (dbmsOutputEnabled && outputConnection != null) cleanupDbmsOutputAfterFailure(outputConnection, error);
|
||||
lobValues.releaseSince(lobCheckpoint);
|
||||
throw error;
|
||||
} finally {
|
||||
captureDbmsOutput = false;
|
||||
dbmsOutputEnabled = false;
|
||||
dbmsOutputWarning = null;
|
||||
deferCharacterLobs = false;
|
||||
}
|
||||
}
|
||||
@@ -134,22 +147,29 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent {
|
||||
@Override
|
||||
public QueryPageResult executeQueryPage(String sql, String schema, QueryPageOptions options) {
|
||||
var lobCheckpoint = lobValues.checkpoint();
|
||||
Connection outputConnection = null;
|
||||
try (QueryTiming timing = QueryTiming.begin()) {
|
||||
prepareLobExecution(sql, options.getDeferLobs());
|
||||
captureDbmsOutput = isDbmsOutputStatement(sql);
|
||||
long prepareStarted = System.nanoTime();
|
||||
Connection connection = requireConnected();
|
||||
outputConnection = requireConnected();
|
||||
Connection connection = outputConnection;
|
||||
uncheckedVoid(() -> beforeQueryExecution(connection, options.getTimeoutSecs()));
|
||||
QueryTiming.record("session_prepare", prepareStarted);
|
||||
QueryPageResult result = JdbcExecutor.current().executeBoundedPage(
|
||||
connection, sql, schema, this::setSchemaSQL, this::resetSchemaSQL, options, resultValueReader()
|
||||
connection, sql, schema, this::setSchemaSQL, this::resetSchemaSQL, options, resultValueReader(), statementMessageReader()
|
||||
);
|
||||
markLobPage(result);
|
||||
result.setQuery_timings_ms(timing.finish());
|
||||
return result;
|
||||
} catch (RuntimeException error) {
|
||||
if (dbmsOutputEnabled && outputConnection != null) cleanupDbmsOutputAfterFailure(outputConnection, error);
|
||||
lobValues.releaseSince(lobCheckpoint);
|
||||
throw error;
|
||||
} finally {
|
||||
captureDbmsOutput = false;
|
||||
dbmsOutputEnabled = false;
|
||||
dbmsOutputWarning = null;
|
||||
deferCharacterLobs = false;
|
||||
}
|
||||
}
|
||||
@@ -342,13 +362,173 @@ public final class OceanBaseOracleAgent extends ConfiguredJdbcAgent {
|
||||
try (var stmt = connection.createStatement()) {
|
||||
try {
|
||||
stmt.execute(queryTimeoutSql(timeoutSecs));
|
||||
queryTimeoutChanged = true;
|
||||
} catch (SQLException error) {
|
||||
if (isReadOnlyTransactionError(error)) {
|
||||
return;
|
||||
if (!isReadOnlyTransactionError(error)) throw error;
|
||||
}
|
||||
}
|
||||
dbmsOutputEnabled = captureDbmsOutput;
|
||||
dbmsOutputWarning = captureDbmsOutput ? enableDbmsOutput(connection) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JdbcExecutor.StatementMessageReader statementMessageReader() {
|
||||
if (!captureDbmsOutput) return JdbcExecutor.StatementMessageReader.NONE;
|
||||
String setupWarning = dbmsOutputWarning;
|
||||
boolean outputEnabled = dbmsOutputEnabled;
|
||||
return new JdbcExecutor.StatementMessageReader() {
|
||||
private boolean cleaned;
|
||||
|
||||
@Override
|
||||
public List<String> read(Statement statement) throws SQLException {
|
||||
List<String> lines = new ArrayList<>();
|
||||
if (!outputEnabled) {
|
||||
if (setupWarning != null) lines.add(setupWarning);
|
||||
return lines;
|
||||
}
|
||||
throw error;
|
||||
int characters = 0;
|
||||
boolean truncated = false;
|
||||
try (CallableStatement call = statement.getConnection().prepareCall("{call DBMS_OUTPUT.GET_LINE(?, ?)}")) {
|
||||
call.setQueryTimeout(2);
|
||||
call.registerOutParameter(1, Types.VARCHAR);
|
||||
call.registerOutParameter(2, Types.INTEGER);
|
||||
long deadline = System.nanoTime() + 5_000_000_000L;
|
||||
while (lines.size() < 256 && characters < 65_536) {
|
||||
if (System.nanoTime() >= deadline) {
|
||||
lines.add("DBMS_OUTPUT truncated after 5 seconds");
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
call.execute();
|
||||
if (call.getInt(2) != 0) break;
|
||||
String line = call.getString(1);
|
||||
if (line == null) line = "";
|
||||
int remaining = 65_536 - characters;
|
||||
if (line.length() > remaining) {
|
||||
int end = remaining;
|
||||
if (end > 0 && Character.isHighSurrogate(line.charAt(end - 1))
|
||||
&& Character.isLowSurrogate(line.charAt(end))) {
|
||||
end--;
|
||||
}
|
||||
lines.add(line.substring(0, end));
|
||||
lines.add("DBMS_OUTPUT truncated at 65536 characters");
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
lines.add(line);
|
||||
characters += line.length();
|
||||
}
|
||||
if (!truncated && (lines.size() == 256 || characters >= 65_536)) {
|
||||
if (System.nanoTime() >= deadline) {
|
||||
lines.add("DBMS_OUTPUT truncated after 5 seconds");
|
||||
} else {
|
||||
call.execute();
|
||||
if (call.getInt(2) == 0) lines.add("DBMS_OUTPUT truncated at the output limit");
|
||||
}
|
||||
}
|
||||
} catch (SQLException ignored) {
|
||||
lines.add("DBMS_OUTPUT could not be read after statement execution");
|
||||
} finally {
|
||||
cleanup(statement, lines);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> close(Statement statement) throws SQLException {
|
||||
if (!outputEnabled) return setupWarning == null ? List.of() : List.of(setupWarning);
|
||||
return read(statement);
|
||||
}
|
||||
|
||||
private void cleanup(Statement statement, List<String> messages) throws SQLException {
|
||||
if (cleaned) return;
|
||||
cleaned = true;
|
||||
appendDbmsOutputCleanupWarning(statement.getConnection(), messages);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean isDbmsOutputStatement(String sql) {
|
||||
if (sql == null) return false;
|
||||
String leading = sql.replaceFirst("(?s)^(?:\\s|--[^\\r\\n]*(?:\\r?\\n|$)|/\\*.*?\\*/)*", "");
|
||||
return leading.matches("(?is)^(?:<<\\s*[\\p{L}_][\\p{L}\\p{N}_$#]*\\s*>>\\s*)?(?:BEGIN|DECLARE|CALL|EXEC|EXECUTE)\\b.*")
|
||||
|| leading.matches("(?is)^\\{\\s*(?:\\?\\s*=\\s*)?CALL\\b.*\\}");
|
||||
}
|
||||
|
||||
private String enableDbmsOutput(Connection connection) {
|
||||
try (var statement = connection.createStatement()) {
|
||||
statement.setQueryTimeout(2);
|
||||
statement.execute("BEGIN DBMS_OUTPUT.DISABLE; DBMS_OUTPUT.ENABLE(NULL); END;");
|
||||
return null;
|
||||
} catch (SQLException error) {
|
||||
if (error instanceof SQLTimeoutException || hasConnectionFailure(error)) {
|
||||
try {
|
||||
discardConnection(connection);
|
||||
} catch (SQLException discardError) {
|
||||
error.addSuppressed(discardError);
|
||||
}
|
||||
captureDbmsOutput = false;
|
||||
dbmsOutputEnabled = false;
|
||||
throw new RuntimeException("DBMS_OUTPUT setup failed; the connection was discarded", error);
|
||||
}
|
||||
if (isExpectedDbmsOutputUnavailable(error)) {
|
||||
dbmsOutputEnabled = false;
|
||||
return "DBMS_OUTPUT could not be enabled; statement execution continued";
|
||||
}
|
||||
throw new RuntimeException("DBMS_OUTPUT setup failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasConnectionFailure(SQLException error) {
|
||||
for (SQLException current = error; current != null; current = current.getNextException()) {
|
||||
String state = current.getSQLState();
|
||||
if (state != null && state.startsWith("08")) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isExpectedDbmsOutputUnavailable(SQLException error) {
|
||||
for (SQLException current = error; current != null; current = current.getNextException()) {
|
||||
int code = Math.abs(current.getErrorCode());
|
||||
String message = current.getMessage();
|
||||
String lower = message == null ? "" : message.toLowerCase(Locale.ROOT);
|
||||
if (code == 1031 || code == 5543 || code == 5546 || lower.contains("insufficient privilege")
|
||||
|| (lower.contains("not declared") || lower.contains("must be declared")) && lower.contains("dbms_output")
|
||||
|| lower.contains("dbms_output") && (lower.contains("not supported") || lower.contains("does not exist"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void appendDbmsOutputCleanupWarning(Connection connection, List<String> messages) {
|
||||
try (var statement = connection.createStatement()) {
|
||||
statement.setQueryTimeout(2);
|
||||
statement.execute("BEGIN DBMS_OUTPUT.DISABLE; END;");
|
||||
} catch (SQLException cleanupError) {
|
||||
try {
|
||||
discardConnection(connection);
|
||||
messages.add("DBMS_OUTPUT cleanup failed; the connection was discarded");
|
||||
} catch (SQLException discardError) {
|
||||
cleanupError.addSuppressed(discardError);
|
||||
messages.add("DBMS_OUTPUT cleanup and connection disposal failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupDbmsOutputAfterFailure(Connection connection, RuntimeException error) {
|
||||
try {
|
||||
try (var statement = connection.createStatement()) {
|
||||
statement.setQueryTimeout(2);
|
||||
statement.execute("BEGIN DBMS_OUTPUT.DISABLE; END;");
|
||||
}
|
||||
} catch (SQLException cleanupError) {
|
||||
error.addSuppressed(cleanupError);
|
||||
try {
|
||||
discardConnection(connection);
|
||||
} catch (SQLException discardError) {
|
||||
error.addSuppressed(discardError);
|
||||
}
|
||||
queryTimeoutChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+382
@@ -22,22 +22,404 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.SQLFeatureNotSupportedException;
|
||||
import java.sql.SQLTimeoutException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
class OceanBaseOracleAgentTest {
|
||||
@Test
|
||||
void returnsDbmsOutputThroughRegularAndBoundedExecutionPaths() throws Exception {
|
||||
assertDbmsOutputExecution(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsDbmsOutputWhenSessionTimeoutIsRejectedAsReadOnly() throws Exception {
|
||||
assertDbmsOutputExecution(true);
|
||||
}
|
||||
|
||||
private void assertDbmsOutputExecution(boolean readOnlyTimeout) throws Exception {
|
||||
String block = "BEGIN DBMS_OUTPUT.PUT_LINE('hello world'); END;";
|
||||
for (boolean paged : List.of(false, true)) {
|
||||
AtomicInteger prepareCalls = new AtomicInteger();
|
||||
AtomicInteger getLineCalls = new AtomicInteger();
|
||||
List<String> executedSql = new ArrayList<>();
|
||||
Connection[] connectionRef = new Connection[1];
|
||||
CallableStatement output = proxy(CallableStatement.class, (method, args) -> {
|
||||
if ("execute".equals(method.getName())) {
|
||||
getLineCalls.incrementAndGet();
|
||||
return false;
|
||||
}
|
||||
if ("getInt".equals(method.getName())) return getLineCalls.get() == 1 ? 0 : 1;
|
||||
if ("getString".equals(method.getName())) return "hello world";
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
Statement statement = proxy(Statement.class, (method, args) -> {
|
||||
if ("execute".equals(method.getName())) {
|
||||
executedSql.add(String.valueOf(args[0]));
|
||||
if (readOnlyTimeout && String.valueOf(args[0]).startsWith("ALTER SESSION SET ob_query_timeout")) {
|
||||
throw new SQLException("read only", "25006");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if ("getConnection".equals(method.getName())) return connectionRef[0];
|
||||
if ("getUpdateCount".equals(method.getName())) return -1;
|
||||
if ("getWarnings".equals(method.getName())) return null;
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
Connection connection = proxy(Connection.class, (method, args) -> {
|
||||
if ("createStatement".equals(method.getName())) return statement;
|
||||
if ("prepareCall".equals(method.getName())) {
|
||||
prepareCalls.incrementAndGet();
|
||||
return output;
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
connectionRef[0] = connection;
|
||||
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||
TestSupport.setPrivateConnection(agent, connection);
|
||||
|
||||
List<String> columns;
|
||||
List<List<Object>> rows;
|
||||
if (paged) {
|
||||
QueryPageResult result = agent.executeQueryPage(block, null, new QueryPageOptions());
|
||||
columns = result.getColumns();
|
||||
rows = result.getRows();
|
||||
} else {
|
||||
QueryResult result = agent.executeQuery(block, null, new ExecuteQueryOptions());
|
||||
columns = result.getColumns();
|
||||
rows = result.getRows();
|
||||
}
|
||||
|
||||
Assertions.assertEquals(List.of("Message"), columns);
|
||||
Assertions.assertEquals(List.of(List.of("hello world")), rows);
|
||||
Assertions.assertEquals(1, prepareCalls.get());
|
||||
Assertions.assertEquals(2, getLineCalls.get());
|
||||
Assertions.assertEquals(1, executedSql.stream().filter(block::equals).count());
|
||||
Assertions.assertTrue(executedSql.contains("BEGIN DBMS_OUTPUT.DISABLE; DBMS_OUTPUT.ENABLE(NULL); END;"));
|
||||
Assertions.assertTrue(executedSql.indexOf("BEGIN DBMS_OUTPUT.DISABLE; DBMS_OUTPUT.ENABLE(NULL); END;")
|
||||
> executedSql.indexOf("ALTER SESSION SET ob_query_timeout"));
|
||||
Assertions.assertEquals("BEGIN DBMS_OUTPUT.DISABLE; END;", executedSql.get(executedSql.size() - 1));
|
||||
|
||||
if (paged) {
|
||||
agent.executeQueryPage("SELECT 1 FROM DUAL", null, new QueryPageOptions());
|
||||
} else {
|
||||
agent.executeQuery("SELECT 1 FROM DUAL", null, new ExecuteQueryOptions());
|
||||
}
|
||||
Assertions.assertEquals(1, prepareCalls.get(), "ordinary SQL must not read DBMS_OUTPUT");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotInstallDbmsOutputReaderForOrdinaryStatements() throws Exception {
|
||||
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||
Method readerMethod = OceanBaseOracleAgent.class.getDeclaredMethod("statementMessageReader");
|
||||
readerMethod.setAccessible(true);
|
||||
|
||||
Assertions.assertSame(
|
||||
com.dbx.agent.JdbcExecutor.StatementMessageReader.NONE,
|
||||
readerMethod.invoke(agent)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void recognizesLabeledAnonymousBlocksForDbmsOutput() throws Exception {
|
||||
Method classify = OceanBaseOracleAgent.class.getDeclaredMethod("isDbmsOutputStatement", String.class);
|
||||
classify.setAccessible(true);
|
||||
|
||||
Assertions.assertEquals(true, classify.invoke(null, "<<output_block>> BEGIN DBMS_OUTPUT.PUT_LINE('x'); END;"));
|
||||
Assertions.assertEquals(true, classify.invoke(null, "{call app.print_output() }"));
|
||||
Assertions.assertEquals(true, classify.invoke(null, "{ ? = call app.get_output() }"));
|
||||
Assertions.assertEquals(false, classify.invoke(null, "SELECT 1 FROM DUAL"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void continuesWhenOceanBaseReportsDbmsOutputAsUndeclared() throws Exception {
|
||||
String block = "BEGIN NULL; END;";
|
||||
AtomicInteger userStatementCalls = new AtomicInteger();
|
||||
AtomicInteger closedConnections = new AtomicInteger();
|
||||
Statement statement = proxy(Statement.class, (method, args) -> {
|
||||
if ("execute".equals(method.getName())) {
|
||||
String sql = String.valueOf(args[0]);
|
||||
if (sql.startsWith("BEGIN DBMS_OUTPUT.DISABLE")) {
|
||||
throw new SQLException("PLS-00201: identifier 'DBMS_OUTPUT' must be declared", "42000", 5543);
|
||||
}
|
||||
if (block.equals(sql)) userStatementCalls.incrementAndGet();
|
||||
return false;
|
||||
}
|
||||
if ("getUpdateCount".equals(method.getName())) return -1;
|
||||
if ("getWarnings".equals(method.getName())) return null;
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
Connection connection = proxy(Connection.class, (method, args) -> {
|
||||
if ("createStatement".equals(method.getName())) return statement;
|
||||
if ("close".equals(method.getName())) {
|
||||
closedConnections.incrementAndGet();
|
||||
return null;
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||
TestSupport.setPrivateConnection(agent, connection);
|
||||
|
||||
QueryResult result = agent.executeQuery(block, null, new ExecuteQueryOptions());
|
||||
|
||||
Assertions.assertEquals(1, userStatementCalls.get());
|
||||
Assertions.assertEquals(0, closedConnections.get());
|
||||
Assertions.assertEquals(List.of("Message"), result.getColumns());
|
||||
Assertions.assertTrue(result.getRows().stream().flatMap(List::stream)
|
||||
.anyMatch(value -> String.valueOf(value).contains("could not be enabled")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotExecuteUserSqlAfterDbmsOutputSetupTimeout() throws Exception {
|
||||
String block = "BEGIN DBMS_OUTPUT.PUT_LINE('hello world'); END;";
|
||||
AtomicInteger userStatementCalls = new AtomicInteger();
|
||||
AtomicInteger closedConnections = new AtomicInteger();
|
||||
Statement statement = proxy(Statement.class, (method, args) -> {
|
||||
if ("execute".equals(method.getName())) {
|
||||
String sql = String.valueOf(args[0]);
|
||||
if (block.equals(sql)) userStatementCalls.incrementAndGet();
|
||||
if (sql.startsWith("ALTER SESSION SET ob_query_timeout")) return false;
|
||||
throw new SQLTimeoutException("setup timed out", "HYT00");
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
Connection connection = proxy(Connection.class, (method, args) -> {
|
||||
if ("createStatement".equals(method.getName())) return statement;
|
||||
if ("close".equals(method.getName())) {
|
||||
closedConnections.incrementAndGet();
|
||||
return null;
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||
TestSupport.setPrivateConnection(agent, connection);
|
||||
|
||||
Assertions.assertThrows(RuntimeException.class,
|
||||
() -> agent.executeQuery(block, null, new ExecuteQueryOptions()));
|
||||
|
||||
Assertions.assertEquals(0, userStatementCalls.get());
|
||||
Assertions.assertEquals(1, closedConnections.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotReportTruncationWhenOutputEndsExactlyAtLimits() throws Exception {
|
||||
assertOutputLimit(List.of());
|
||||
assertOutputLimit(java.util.Collections.nCopies(256, "x"));
|
||||
assertOutputLimit(List.of("x".repeat(32_767), "x".repeat(32_767), "x".repeat(2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotSplitSupplementaryUnicodeWhenTruncatingOutput() throws Exception {
|
||||
List<String> messages = readOutputLines(List.of("x".repeat(65_535), "😀"));
|
||||
|
||||
Assertions.assertEquals(65_535, messages.get(0).length());
|
||||
Assertions.assertFalse(messages.get(0).contains("\uD800"));
|
||||
Assertions.assertEquals("", messages.get(1));
|
||||
Assertions.assertTrue(messages.get(2).contains("truncated"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsTruncationOnlyWhenAnotherLineExistsPastLineLimit() throws Exception {
|
||||
List<String> outputLines = new ArrayList<>(java.util.Collections.nCopies(257, "x"));
|
||||
AtomicInteger callCount = new AtomicInteger();
|
||||
Connection[] connectionRef = new Connection[1];
|
||||
CallableStatement output = proxy(CallableStatement.class, (method, args) -> {
|
||||
if ("execute".equals(method.getName())) {
|
||||
callCount.incrementAndGet();
|
||||
return false;
|
||||
}
|
||||
if ("getInt".equals(method.getName())) return callCount.get() <= outputLines.size() ? 0 : 1;
|
||||
if ("getString".equals(method.getName())) return outputLines.get(callCount.get() - 1);
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
Statement statement = proxy(Statement.class, (method, args) ->
|
||||
"getConnection".equals(method.getName()) ? connectionRef[0] : defaultValue(method.getReturnType()));
|
||||
Connection connection = proxy(Connection.class, (method, args) -> {
|
||||
if ("prepareCall".equals(method.getName())) return output;
|
||||
if ("createStatement".equals(method.getName())) return proxy(Statement.class, (m, a) -> defaultValue(m.getReturnType()));
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
connectionRef[0] = connection;
|
||||
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||
setField(agent, "captureDbmsOutput", true);
|
||||
setField(agent, "dbmsOutputEnabled", true);
|
||||
Method readerMethod = OceanBaseOracleAgent.class.getDeclaredMethod("statementMessageReader");
|
||||
readerMethod.setAccessible(true);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> messages = ((com.dbx.agent.JdbcExecutor.StatementMessageReader) readerMethod.invoke(agent)).read(statement);
|
||||
|
||||
Assertions.assertEquals(257, callCount.get());
|
||||
Assertions.assertEquals(257, messages.size());
|
||||
Assertions.assertEquals("DBMS_OUTPUT truncated at the output limit", messages.get(256));
|
||||
}
|
||||
|
||||
private static void assertOutputLimit(List<String> outputLines) throws Exception {
|
||||
List<String> messages = readOutputLines(outputLines);
|
||||
Assertions.assertEquals(outputLines, messages);
|
||||
Assertions.assertFalse(messages.stream().anyMatch(message -> message.contains("truncated")));
|
||||
}
|
||||
|
||||
private static List<String> readOutputLines(List<String> outputLines) throws Exception {
|
||||
AtomicInteger callCount = new AtomicInteger();
|
||||
Connection[] connectionRef = new Connection[1];
|
||||
CallableStatement output = proxy(CallableStatement.class, (method, args) -> {
|
||||
if ("execute".equals(method.getName())) {
|
||||
callCount.incrementAndGet();
|
||||
return false;
|
||||
}
|
||||
if ("getInt".equals(method.getName())) return callCount.get() <= outputLines.size() ? 0 : 1;
|
||||
if ("getString".equals(method.getName())) return outputLines.get(callCount.get() - 1);
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
Statement statement = proxy(Statement.class, (method, args) ->
|
||||
"getConnection".equals(method.getName()) ? connectionRef[0] : defaultValue(method.getReturnType()));
|
||||
Connection connection = proxy(Connection.class, (method, args) -> {
|
||||
if ("prepareCall".equals(method.getName())) return output;
|
||||
if ("createStatement".equals(method.getName())) return proxy(Statement.class, (m, a) -> defaultValue(m.getReturnType()));
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
connectionRef[0] = connection;
|
||||
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||
setField(agent, "captureDbmsOutput", true);
|
||||
setField(agent, "dbmsOutputEnabled", true);
|
||||
Method readerMethod = OceanBaseOracleAgent.class.getDeclaredMethod("statementMessageReader");
|
||||
readerMethod.setAccessible(true);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> messages = ((com.dbx.agent.JdbcExecutor.StatementMessageReader) readerMethod.invoke(agent)).read(statement);
|
||||
|
||||
int characters = outputLines.stream().mapToInt(String::length).sum();
|
||||
int expectedCalls = outputLines.size() > 256 || characters > 65_536
|
||||
? Math.min(outputLines.size(), 256)
|
||||
: outputLines.size() + 1;
|
||||
Assertions.assertEquals(expectedCalls, callCount.get());
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Test
|
||||
void discardsConnectionWhenDbmsOutputCleanupFails() throws Exception {
|
||||
String block = "BEGIN DBMS_OUTPUT.PUT_LINE('hello world'); END;";
|
||||
AtomicInteger outputSetupCalls = new AtomicInteger();
|
||||
AtomicInteger userStatementCalls = new AtomicInteger();
|
||||
AtomicInteger closedConnections = new AtomicInteger();
|
||||
Connection[] connectionRef = new Connection[1];
|
||||
CallableStatement output = proxy(CallableStatement.class, (method, args) -> {
|
||||
if ("execute".equals(method.getName())) throw new SQLException("GET_LINE failed");
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
Statement statement = proxy(Statement.class, (method, args) -> {
|
||||
if ("execute".equals(method.getName())) {
|
||||
String sql = String.valueOf(args[0]);
|
||||
if (sql.startsWith("BEGIN DBMS_OUTPUT.DISABLE")) {
|
||||
if (outputSetupCalls.incrementAndGet() > 1) throw new SQLException("cleanup failed");
|
||||
}
|
||||
if (block.equals(sql)) userStatementCalls.incrementAndGet();
|
||||
return false;
|
||||
}
|
||||
if ("getConnection".equals(method.getName())) return connectionRef[0];
|
||||
if ("getUpdateCount".equals(method.getName())) return -1;
|
||||
if ("getWarnings".equals(method.getName())) return null;
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
Connection connection = proxy(Connection.class, (method, args) -> {
|
||||
if ("createStatement".equals(method.getName())) return statement;
|
||||
if ("prepareCall".equals(method.getName())) return output;
|
||||
if ("close".equals(method.getName())) {
|
||||
closedConnections.incrementAndGet();
|
||||
return null;
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
connectionRef[0] = connection;
|
||||
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||
TestSupport.setPrivateConnection(agent, connection);
|
||||
|
||||
QueryResult result = agent.executeQuery(block, null, new ExecuteQueryOptions());
|
||||
|
||||
Assertions.assertEquals(List.of("Message"), result.getColumns());
|
||||
Assertions.assertTrue(result.getRows().stream().flatMap(List::stream)
|
||||
.anyMatch(value -> String.valueOf(value).contains("DBMS_OUTPUT cleanup failed")));
|
||||
Assertions.assertEquals(1, closedConnections.get(), "a connection with unknown DBMS_OUTPUT state must be discarded");
|
||||
Assertions.assertEquals(2, outputSetupCalls.get(), "one initial enable and one failed reset are expected");
|
||||
Assertions.assertEquals(1, userStatementCalls.get(), "cleanup failure must not retry the user statement");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleansDbmsOutputWhenPagedPlsqlReturnsAResultSet() throws Exception {
|
||||
String block = "BEGIN OPEN :cursor FOR SELECT 1 FROM DUAL; END;";
|
||||
List<String> executedSql = new ArrayList<>();
|
||||
int[] row = {-1};
|
||||
AtomicInteger outputCalls = new AtomicInteger();
|
||||
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] == 0;
|
||||
if ("getMetaData".equals(method.getName())) return meta;
|
||||
if ("getObject".equals(method.getName()) || "getInt".equals(method.getName())) return 1;
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
CallableStatement output = proxy(CallableStatement.class, (method, args) -> {
|
||||
if ("execute".equals(method.getName())) {
|
||||
outputCalls.incrementAndGet();
|
||||
return false;
|
||||
}
|
||||
if ("getInt".equals(method.getName())) return outputCalls.get() == 1 ? 0 : 1;
|
||||
if ("getString".equals(method.getName())) return "cursor output";
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
Connection[] connectionRef = new Connection[1];
|
||||
Statement statement = proxy(Statement.class, (method, args) -> {
|
||||
if ("execute".equals(method.getName())) {
|
||||
String sql = String.valueOf(args[0]);
|
||||
executedSql.add(sql);
|
||||
return block.equals(sql);
|
||||
}
|
||||
if ("getResultSet".equals(method.getName())) return cursor;
|
||||
if ("getConnection".equals(method.getName())) return connectionRef[0];
|
||||
if ("getUpdateCount".equals(method.getName())) return -1;
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
Connection connection = proxy(Connection.class, (method, args) -> {
|
||||
if ("createStatement".equals(method.getName())) return statement;
|
||||
if ("prepareCall".equals(method.getName())) return output;
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
connectionRef[0] = connection;
|
||||
OceanBaseOracleAgent agent = new OceanBaseOracleAgent();
|
||||
TestSupport.setPrivateConnection(agent, connection);
|
||||
|
||||
QueryPageResult result = agent.executeQueryPage(block, null, new QueryPageOptions(10, null, 10, 0));
|
||||
|
||||
Assertions.assertEquals(List.of("N"), result.getColumns());
|
||||
Assertions.assertEquals(List.of(List.of(1)), result.getRows());
|
||||
Assertions.assertTrue(result.getMessages().stream().anyMatch(message -> "cursor output".equals(message.get("message"))));
|
||||
Assertions.assertEquals(2, outputCalls.get());
|
||||
Assertions.assertTrue(executedSql.contains("BEGIN DBMS_OUTPUT.DISABLE; END;"),
|
||||
"closing the completed result-set session must disable DBMS_OUTPUT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildsOceanBaseJdbcUrl() {
|
||||
ConnectParams params = new ConnectParams();
|
||||
|
||||
Reference in New Issue
Block a user