mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
fix(jdbc): honor query execution settings
This commit is contained in:
@@ -286,6 +286,17 @@ impl AgentDriverClient {
|
||||
&mut self,
|
||||
method: &str,
|
||||
params: Value,
|
||||
) -> Result<T, String> {
|
||||
self.call_with_timeout(method, params, Some(Duration::from_secs(RPC_TIMEOUT_SECS))).await
|
||||
}
|
||||
|
||||
/// Send a JSON-RPC 2.0 request and wait for the response.
|
||||
/// `None` disables the client-side RPC timeout for long-running query calls.
|
||||
pub async fn call_with_timeout<T: DeserializeOwned + Send + 'static>(
|
||||
&mut self,
|
||||
method: &str,
|
||||
params: Value,
|
||||
timeout_duration: Option<Duration>,
|
||||
) -> Result<T, String> {
|
||||
self.next_id += 1;
|
||||
let id = self.next_id;
|
||||
@@ -317,37 +328,42 @@ impl AgentDriverClient {
|
||||
// Read response from stdout (blocking, with timeout)
|
||||
let mut reader = self.stdout.take().ok_or("Agent stdout not available")?;
|
||||
|
||||
let (returned_reader, result) = tokio::time::timeout(
|
||||
Duration::from_secs(RPC_TIMEOUT_SECS),
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let line = match read_agent_line(&mut reader, "response") {
|
||||
Ok(line) => line,
|
||||
Err(e) => return (reader, Err(e)),
|
||||
};
|
||||
let response_task = tokio::task::spawn_blocking(move || {
|
||||
let line = match read_agent_line(&mut reader, "response") {
|
||||
Ok(line) => line,
|
||||
Err(e) => return (reader, Err(e)),
|
||||
};
|
||||
|
||||
let resp: Value = match serde_json::from_str(line.trim()) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return (reader, Err(format!("Invalid JSON response from agent: {e}")));
|
||||
}
|
||||
};
|
||||
let resp: Value = match serde_json::from_str(line.trim()) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return (reader, Err(format!("Invalid JSON response from agent: {e}")));
|
||||
}
|
||||
};
|
||||
|
||||
let result = if let Some(err) = resp.get("error") {
|
||||
let msg = err.get("message").and_then(|m| m.as_str()).unwrap_or("Unknown agent error");
|
||||
let code = err.get("code").and_then(|c| c.as_i64()).unwrap_or(-1);
|
||||
Err(format!("Agent RPC error ({code}): {msg}"))
|
||||
} else if let Some(result_val) = resp.get("result") {
|
||||
serde_json::from_value::<T>(result_val.clone())
|
||||
.map_err(|e| format!("Failed to deserialize agent result: {e}"))
|
||||
} else {
|
||||
Err(format!("Agent response missing both 'result' and 'error': {line}"))
|
||||
};
|
||||
let result = if let Some(err) = resp.get("error") {
|
||||
let msg = err.get("message").and_then(|m| m.as_str()).unwrap_or("Unknown agent error");
|
||||
let code = err.get("code").and_then(|c| c.as_i64()).unwrap_or(-1);
|
||||
Err(format!("Agent RPC error ({code}): {msg}"))
|
||||
} else if let Some(result_val) = resp.get("result") {
|
||||
serde_json::from_value::<T>(result_val.clone())
|
||||
.map_err(|e| format!("Failed to deserialize agent result: {e}"))
|
||||
} else {
|
||||
Err(format!("Agent response missing both 'result' and 'error': {line}"))
|
||||
};
|
||||
|
||||
(reader, result)
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| format!("Agent RPC call timed out ({RPC_TIMEOUT_SECS}s)"))?
|
||||
(reader, result)
|
||||
});
|
||||
let (returned_reader, result) = match timeout_duration {
|
||||
Some(duration) => match tokio::time::timeout(duration, response_task).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
self.kill();
|
||||
return Err(format!("Agent RPC call timed out ({}s)", duration.as_secs()));
|
||||
}
|
||||
},
|
||||
None => response_task.await,
|
||||
}
|
||||
.map_err(|e| format!("Agent RPC task failed: {e}"))?;
|
||||
|
||||
let _ = self.stdout.insert(returned_reader);
|
||||
@@ -362,6 +378,15 @@ impl AgentDriverClient {
|
||||
self.call(method.as_str(), params).await
|
||||
}
|
||||
|
||||
pub async fn call_method_with_timeout<T: DeserializeOwned + Send + 'static>(
|
||||
&mut self,
|
||||
method: AgentMethod,
|
||||
params: Value,
|
||||
timeout_duration: Option<Duration>,
|
||||
) -> Result<T, String> {
|
||||
self.call_with_timeout(method.as_str(), params, timeout_duration).await
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self, params: Value) -> Result<Value, String> {
|
||||
self.call_method(AgentMethod::Connect, params).await
|
||||
}
|
||||
@@ -458,6 +483,14 @@ impl AgentDriverClient {
|
||||
self.call_method(AgentMethod::ExecuteQuery, params).await
|
||||
}
|
||||
|
||||
pub async fn execute_query_with_timeout<T: DeserializeOwned + Send + 'static>(
|
||||
&mut self,
|
||||
params: Value,
|
||||
timeout_duration: Option<Duration>,
|
||||
) -> Result<T, String> {
|
||||
self.call_method_with_timeout(AgentMethod::ExecuteQuery, params, timeout_duration).await
|
||||
}
|
||||
|
||||
pub async fn execute_query_page<T: DeserializeOwned + Send + 'static>(
|
||||
&mut self,
|
||||
params: Value,
|
||||
@@ -465,10 +498,26 @@ impl AgentDriverClient {
|
||||
self.call_method(AgentMethod::ExecuteQueryPage, params).await
|
||||
}
|
||||
|
||||
pub async fn execute_query_page_with_timeout<T: DeserializeOwned + Send + 'static>(
|
||||
&mut self,
|
||||
params: Value,
|
||||
timeout_duration: Option<Duration>,
|
||||
) -> Result<T, String> {
|
||||
self.call_method_with_timeout(AgentMethod::ExecuteQueryPage, params, timeout_duration).await
|
||||
}
|
||||
|
||||
pub async fn fetch_query_page<T: DeserializeOwned + Send + 'static>(&mut self, params: Value) -> Result<T, String> {
|
||||
self.call_method(AgentMethod::FetchQueryPage, params).await
|
||||
}
|
||||
|
||||
pub async fn fetch_query_page_with_timeout<T: DeserializeOwned + Send + 'static>(
|
||||
&mut self,
|
||||
params: Value,
|
||||
timeout_duration: Option<Duration>,
|
||||
) -> Result<T, String> {
|
||||
self.call_method_with_timeout(AgentMethod::FetchQueryPage, params, timeout_duration).await
|
||||
}
|
||||
|
||||
pub async fn close_query_session<T: DeserializeOwned + Send + 'static>(
|
||||
&mut self,
|
||||
session_id: &str,
|
||||
|
||||
@@ -242,26 +242,35 @@ impl PluginDriverSession {
|
||||
}
|
||||
|
||||
pub async fn invoke<T>(&self, method: &str, params: serde_json::Value) -> Result<T, String>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
self.invoke_with_timeout(method, params, Some(PLUGIN_REQUEST_TIMEOUT)).await
|
||||
}
|
||||
|
||||
pub async fn invoke_with_timeout<T>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: serde_json::Value,
|
||||
timeout_duration: Option<Duration>,
|
||||
) -> Result<T, String>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
|
||||
let result = timeout(PLUGIN_REQUEST_TIMEOUT, async {
|
||||
let invoke = async {
|
||||
let mut process = self.process.lock().await;
|
||||
self.invoke_locked(&mut process, request_id, method, params).await
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
self.kill().await;
|
||||
Err(format!(
|
||||
"Plugin '{}' timed out after {} seconds",
|
||||
self.plugin.manifest.id,
|
||||
PLUGIN_REQUEST_TIMEOUT.as_secs()
|
||||
))
|
||||
}
|
||||
};
|
||||
match timeout_duration {
|
||||
Some(duration) => match timeout(duration, invoke).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
self.kill().await;
|
||||
Err(format!("Plugin '{}' timed out after {} seconds", self.plugin.manifest.id, duration.as_secs()))
|
||||
}
|
||||
},
|
||||
None => invoke.await,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -316,6 +316,9 @@ pub fn agent_execute_query_params(
|
||||
if let Some(fetch_size) = options.fetch_size {
|
||||
params["fetchSize"] = serde_json::json!(fetch_size);
|
||||
}
|
||||
if let Some(timeout_secs) = options.timeout_secs {
|
||||
params["timeoutSecs"] = serde_json::json!(timeout_secs);
|
||||
}
|
||||
params
|
||||
}
|
||||
|
||||
@@ -339,6 +342,9 @@ pub fn agent_execute_query_page_params(
|
||||
if let Some(fetch_size) = options.fetch_size {
|
||||
params["fetchSize"] = serde_json::json!(fetch_size);
|
||||
}
|
||||
if let Some(timeout_secs) = options.timeout_secs {
|
||||
params["timeoutSecs"] = serde_json::json!(timeout_secs);
|
||||
}
|
||||
params
|
||||
}
|
||||
|
||||
@@ -580,18 +586,19 @@ pub async fn do_execute(
|
||||
let database = database.map(|s| s.to_string());
|
||||
let schema = schema.map(|s| s.to_string());
|
||||
let max_rows = options.max_rows;
|
||||
let rpc_timeout = query_timeout;
|
||||
drop(connections);
|
||||
wait_for_query_opt(cancel_token, query_timeout, async move {
|
||||
let mut client = client.lock().await;
|
||||
if let Some(session_id) = options.result_session_id.as_deref() {
|
||||
let params = agent_fetch_query_page_params(session_id, options.page_size.unwrap_or(MAX_ROWS));
|
||||
client.fetch_query_page(params).await
|
||||
client.fetch_query_page_with_timeout(params, rpc_timeout).await
|
||||
} else if options.page_size.is_some() {
|
||||
let params = agent_execute_query_page_params(&sql, database.as_deref(), schema.as_deref(), options);
|
||||
client.execute_query_page(params).await
|
||||
client.execute_query_page_with_timeout(params, rpc_timeout).await
|
||||
} else {
|
||||
let params = agent_execute_query_params(&sql, database.as_deref(), schema.as_deref(), options);
|
||||
client.execute_query(params).await
|
||||
client.execute_query_with_timeout(params, rpc_timeout).await
|
||||
}
|
||||
})
|
||||
.await
|
||||
@@ -621,10 +628,12 @@ pub async fn do_execute(
|
||||
let schema = schema.map(str::to_string);
|
||||
let database = config.effective_database().unwrap_or("").to_string();
|
||||
let max_rows = options.max_rows;
|
||||
let plugin_timeout = query_timeout;
|
||||
drop(connections);
|
||||
wait_for_query_opt(cancel_token, query_timeout, async move {
|
||||
let params = external_driver_query_params(config.as_ref(), &sql, &database, schema.as_deref());
|
||||
session.invoke::<db::QueryResult>("executeQuery", params).await
|
||||
let params =
|
||||
external_driver_query_params(config.as_ref(), &sql, &database, schema.as_deref(), &options);
|
||||
session.invoke_with_timeout::<db::QueryResult>("executeQuery", params, plugin_timeout).await
|
||||
})
|
||||
.await
|
||||
.map(|result| normalize_query_result_for_js(truncate_result_with_max_rows(result, max_rows)))
|
||||
@@ -637,13 +646,22 @@ fn external_driver_query_params(
|
||||
sql: &str,
|
||||
database: &str,
|
||||
schema: Option<&str>,
|
||||
options: &QueryExecutionOptions,
|
||||
) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
let mut params = serde_json::json!({
|
||||
"connection": config,
|
||||
"sql": sql,
|
||||
"database": database,
|
||||
"schema": schema,
|
||||
})
|
||||
"maxRows": options.max_rows.unwrap_or(MAX_ROWS),
|
||||
});
|
||||
if let Some(fetch_size) = options.fetch_size {
|
||||
params["fetchSize"] = serde_json::json!(fetch_size);
|
||||
}
|
||||
if let Some(timeout_secs) = options.timeout_secs {
|
||||
params["timeoutSecs"] = serde_json::json!(timeout_secs);
|
||||
}
|
||||
params
|
||||
}
|
||||
|
||||
pub async fn execute_sql_statement(
|
||||
@@ -1534,12 +1552,26 @@ mod tests {
|
||||
one_time: false,
|
||||
};
|
||||
|
||||
let params = external_driver_query_params(&config, "SELECT * FROM events", "analytics", Some("app"));
|
||||
let params = external_driver_query_params(
|
||||
&config,
|
||||
"SELECT * FROM events",
|
||||
"analytics",
|
||||
Some("app"),
|
||||
&QueryExecutionOptions {
|
||||
max_rows: Some(500),
|
||||
fetch_size: Some(250),
|
||||
timeout_secs: Some(600),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(params["connection"]["id"], "jdbc-1");
|
||||
assert_eq!(params["sql"], "SELECT * FROM events");
|
||||
assert_eq!(params["database"], "analytics");
|
||||
assert_eq!(params["schema"], "app");
|
||||
assert_eq!(params["maxRows"], 500);
|
||||
assert_eq!(params["fetchSize"], 250);
|
||||
assert_eq!(params["timeoutSecs"], 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1548,7 +1580,12 @@ mod tests {
|
||||
"SELECT * FROM events",
|
||||
Some("analytics"),
|
||||
Some("app"),
|
||||
QueryExecutionOptions { max_rows: Some(500), fetch_size: Some(250), ..Default::default() },
|
||||
QueryExecutionOptions {
|
||||
max_rows: Some(500),
|
||||
fetch_size: Some(250),
|
||||
timeout_secs: Some(600),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(params["sql"], "SELECT * FROM events");
|
||||
@@ -1556,6 +1593,7 @@ mod tests {
|
||||
assert_eq!(params["schema"], "app");
|
||||
assert_eq!(params["maxRows"], 500);
|
||||
assert_eq!(params["fetchSize"], 250);
|
||||
assert_eq!(params["timeoutSecs"], 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1567,6 +1605,7 @@ mod tests {
|
||||
assert!(params.get("schema").is_none());
|
||||
assert_eq!(params["maxRows"], MAX_ROWS);
|
||||
assert!(params.get("fetchSize").is_none());
|
||||
assert!(params.get("timeoutSecs").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1575,7 +1614,12 @@ mod tests {
|
||||
"SELECT * FROM events",
|
||||
Some("analytics"),
|
||||
Some("app"),
|
||||
QueryExecutionOptions { page_size: Some(500), fetch_size: Some(250), ..Default::default() },
|
||||
QueryExecutionOptions {
|
||||
page_size: Some(500),
|
||||
fetch_size: Some(250),
|
||||
timeout_secs: Some(600),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(params["sql"], "SELECT * FROM events");
|
||||
@@ -1583,6 +1627,7 @@ mod tests {
|
||||
assert_eq!(params["schema"], "app");
|
||||
assert_eq!(params["pageSize"], 500);
|
||||
assert_eq!(params["fetchSize"], 250);
|
||||
assert_eq!(params["timeoutSecs"], 600);
|
||||
assert_eq!(params["maxRows"], MAX_ROWS);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,4 +17,8 @@ else
|
||||
exit 127
|
||||
fi
|
||||
|
||||
exec "$JAVA_BIN" -jar "$DIR/../lib/dbx-jdbc-plugin.jar"
|
||||
exec "$JAVA_BIN" \
|
||||
-Dfile.encoding=UTF-8 \
|
||||
-Dsun.stdout.encoding=UTF-8 \
|
||||
-Dsun.stderr.encoding=UTF-8 \
|
||||
-jar "$DIR/../lib/dbx-jdbc-plugin.jar"
|
||||
|
||||
@@ -28,4 +28,4 @@ echo Java runtime not found. Install Java or the optional DBX JDBC runtime. >&2
|
||||
exit /b 127
|
||||
|
||||
:run
|
||||
"%JAVA_BIN%" -jar "%~dp0..\lib\dbx-jdbc-plugin.jar"
|
||||
"%JAVA_BIN%" -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -jar "%~dp0..\lib\dbx-jdbc-plugin.jar"
|
||||
|
||||
@@ -6,7 +6,9 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.URLEncoder;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URL;
|
||||
@@ -62,15 +64,19 @@ public final class DbxJdbcPlugin {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
|
||||
try (
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8))
|
||||
) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
ObjectNode response = handleLine(line);
|
||||
System.out.println(MAPPER.writeValueAsString(response));
|
||||
System.out.flush();
|
||||
writer.write(MAPPER.writeValueAsString(response));
|
||||
writer.newLine();
|
||||
writer.flush();
|
||||
if (response.path("_dbx_close").asBoolean(false)) {
|
||||
break;
|
||||
}
|
||||
@@ -120,7 +126,10 @@ public final class DbxJdbcPlugin {
|
||||
connection,
|
||||
requireText(params, "sql"),
|
||||
optionalText(params, "database"),
|
||||
optionalText(params, "schema")
|
||||
optionalText(params, "schema"),
|
||||
positiveInt(params, "maxRows", MAX_ROWS),
|
||||
nonNegativeInt(params, "fetchSize", 0),
|
||||
nonNegativeInt(params, "timeoutSecs", -1)
|
||||
);
|
||||
case "listDatabases" -> listDatabases(connection);
|
||||
case "listSchemas" -> listSchemas(connection, optionalText(params, "database"));
|
||||
@@ -218,12 +227,20 @@ public final class DbxJdbcPlugin {
|
||||
return sharedConnection;
|
||||
}
|
||||
|
||||
private static JsonNode executeQuery(JsonNode connection, String sql, String database, String schema) throws SQLException {
|
||||
private static JsonNode executeQuery(
|
||||
JsonNode connection,
|
||||
String sql,
|
||||
String database,
|
||||
String schema,
|
||||
int maxRows,
|
||||
int fetchSize,
|
||||
int timeoutSecs
|
||||
) throws SQLException {
|
||||
long start = System.nanoTime();
|
||||
Connection conn = openConnection(connection);
|
||||
applyExecutionContext(connection, conn, database, schema);
|
||||
try (Statement statement = conn.createStatement()) {
|
||||
statement.setMaxRows(MAX_ROWS + 1);
|
||||
applyStatementOptions(statement, maxRows, fetchSize, timeoutSecs);
|
||||
boolean hasResultSet = statement.execute(trimStatementSql(sql));
|
||||
ObjectNode result = MAPPER.createObjectNode();
|
||||
ArrayNode columns = MAPPER.createArrayNode();
|
||||
@@ -239,7 +256,7 @@ public final class DbxJdbcPlugin {
|
||||
columns.add(label == null || label.isBlank() ? meta.getColumnName(i) : label);
|
||||
}
|
||||
while (rs.next()) {
|
||||
if (rows.size() >= MAX_ROWS) {
|
||||
if (rows.size() >= maxRows) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
@@ -261,6 +278,23 @@ public final class DbxJdbcPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyStatementOptions(Statement statement, int maxRows, int fetchSize, int timeoutSecs)
|
||||
throws SQLException {
|
||||
statement.setMaxRows((int) Math.min(Integer.MAX_VALUE, (long) maxRows + 1L));
|
||||
if (fetchSize > 0) {
|
||||
try {
|
||||
statement.setFetchSize(fetchSize);
|
||||
} catch (SQLFeatureNotSupportedException ignored) {
|
||||
}
|
||||
}
|
||||
if (timeoutSecs >= 0) {
|
||||
try {
|
||||
statement.setQueryTimeout(timeoutSecs);
|
||||
} catch (SQLFeatureNotSupportedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String trimStatementSql(String sql) {
|
||||
return sql == null ? "" : sql.trim().replaceFirst(";\\s*$", "");
|
||||
}
|
||||
@@ -927,6 +961,21 @@ public final class DbxJdbcPlugin {
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private static int positiveInt(JsonNode node, String field, int defaultValue) {
|
||||
return Math.max(1, nonNegativeInt(node, field, defaultValue));
|
||||
}
|
||||
|
||||
private static int nonNegativeInt(JsonNode node, String field, int defaultValue) {
|
||||
JsonNode value = node.path(field);
|
||||
if (value.isMissingNode() || value.isNull()) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (!value.canConvertToInt()) {
|
||||
return defaultValue;
|
||||
}
|
||||
return Math.max(0, value.asInt(defaultValue));
|
||||
}
|
||||
|
||||
private static String emptyToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,36 @@ final class DbxJdbcPluginTest {
|
||||
assertEquals("0x0001abff", response.path("result").path("rows").path(0).path(0).asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeQueryPreservesChineseTextValues() throws Exception {
|
||||
JsonNode response = request("executeQuery", """
|
||||
{
|
||||
"connection": %s,
|
||||
"sql": "SELECT '中文测试' AS label"
|
||||
}
|
||||
""".formatted(CONNECTION));
|
||||
|
||||
assertFalse(response.has("error"), response.toString());
|
||||
assertEquals("中文测试", response.path("result").path("rows").path(0).path(0).asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeQueryHonorsMaxRowsAndAcceptsExecutionOptions() throws Exception {
|
||||
JsonNode response = request("executeQuery", """
|
||||
{
|
||||
"connection": %s,
|
||||
"sql": "SELECT * FROM (VALUES (1), (2)) AS t(n)",
|
||||
"maxRows": 1,
|
||||
"fetchSize": 1,
|
||||
"timeoutSecs": 60
|
||||
}
|
||||
""".formatted(CONNECTION));
|
||||
|
||||
assertFalse(response.has("error"), response.toString());
|
||||
assertEquals(1, response.path("result").path("rows").size());
|
||||
assertEquals(true, response.path("result").path("truncated").asBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void driverQuirksDetectYashanJdbcUrl() throws Exception {
|
||||
JsonNode yashan = MAPPER.readTree("""
|
||||
|
||||
Reference in New Issue
Block a user