Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c48a901
feat: enhance Oracle connection form with 3 connection methods and ro…
Blankll Jun 19, 2026
65a3890
fix: show full TNS alias names as-is, no level suffix stripping
Blankll Jun 19, 2026
8f5930a
fix: Oracle connection form issues from review
Blankll Jun 20, 2026
4698b63
feat: Oracle Cloud Wallet (ATP/ADW) connection support
Blankll Jun 20, 2026
c0e7717
fix(jdbc-bridge): cross-database audit fixes
Blankll Jun 20, 2026
c337f5d
feat(connection): RwLock, timeouts, health guardian, ConnectionHandle…
Blankll Jun 20, 2026
ed34843
fix(guardian): spawn via tauri::async_runtime instead of tokio::spawn
Blankll Jun 20, 2026
e3b578f
fix(ui): add drag spacer to header for window dragging
Blankll Jun 20, 2026
353dc41
fix(ui): set decorations:false on macOS to enable header drag
Blankll Jun 20, 2026
eba754d
fix(ui): remove data-tauri-drag-region, let native titlebar handle drag
Blankll Jun 20, 2026
8a21abe
fix(ui): add explicit -webkit-app-region:drag CSS rule
Blankll Jun 20, 2026
e0a6cf3
fix(ui): body-level -webkit-app-region:drag with no-drag on interacti…
Blankll Jun 20, 2026
898b8a2
chore: config
Blankll Jun 20, 2026
6607874
fix(connection): wire up dead code — GUARDIAN, ConnectionHandle trait…
Blankll Jun 20, 2026
48ecabc
fix(connection): P0 connect timeout + P1 evict_idle logic bug
Blankll Jun 20, 2026
5634bf1
fix(connection): wire LRU cache into query flow + explain guardian check
Blankll Jun 20, 2026
1067f41
fix(mysql): list_schemas regression — return only requested database
Blankll Jun 20, 2026
339e442
fix(browse): complete trait migration — remaining match arms in brows…
Blankll Jun 20, 2026
47da5aa
fix(jdbc): expand download_jdbc_driver_direct to all 22 JDBC databases
Blankll Jun 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions jdbc-bridge/src/main/java/sqlkit/bridge/ConnectionManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,35 @@ public void connect(String connId, String url, String username,

DriverClassLoader loader = new DriverClassLoader(driverJars);
Class<?> driverCls = Class.forName(driverClass, true, loader);
if (java.sql.Driver.class.isAssignableFrom(driverCls)) {
java.sql.Driver driver = (java.sql.Driver) driverCls.getDeclaredConstructor().newInstance();
java.sql.DriverManager.registerDriver(driver);
if (!java.sql.Driver.class.isAssignableFrom(driverCls)) {
throw new ClassifiedException("Class " + driverClass + " does not implement java.sql.Driver", null, ErrorClassifier.ErrorType.UNKNOWN);
}
final java.sql.Driver driver = (java.sql.Driver) driverCls.getDeclaredConstructor().newInstance();
loaders.put(connId, loader);

HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setDataSource(new javax.sql.DataSource() {
public java.sql.Connection getConnection() throws java.sql.SQLException {
java.util.Properties info = new java.util.Properties();
if (username != null) info.setProperty("user", username);
if (password != null) info.setProperty("password", password);
return driver.connect(url, info);
}
public java.sql.Connection getConnection(String u, String p) throws java.sql.SQLException {
java.util.Properties info = new java.util.Properties();
info.setProperty("user", u);
info.setProperty("password", p);
return driver.connect(url, info);
}
public java.io.PrintWriter getLogWriter() { return null; }
public void setLogWriter(java.io.PrintWriter out) {}
public void setLoginTimeout(int seconds) {}
public int getLoginTimeout() { return 0; }
public java.util.logging.Logger getParentLogger() { return java.util.logging.Logger.getLogger("sqlkit.bridge"); }
@SuppressWarnings("unchecked")
public <T> T unwrap(Class<T> iface) throws java.sql.SQLException { throw new java.sql.SQLException("Not supported"); }
public boolean isWrapperFor(Class<?> iface) { return false; }
});
config.setUsername(username);
if (password != null && !password.isEmpty()) {
config.setPassword(password);
Expand All @@ -53,9 +74,6 @@ public void connect(String connId, String url, String username,
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

HikariDataSource ds = new HikariDataSource(config);

Expand All @@ -64,7 +82,7 @@ public void connect(String connId, String url, String username,
// ok
} catch (Exception e) {
ds.close();
ErrorClassifier.ErrorType errorType = ErrorClassifier.classify(e.getMessage());
ErrorClassifier.ErrorType errorType = ErrorClassifier.classify(e);
throw new ClassifiedException("Failed to verify connection: " + e.getMessage(), e, errorType);
}

Expand Down
47 changes: 42 additions & 5 deletions jdbc-bridge/src/main/java/sqlkit/bridge/ErrorClassifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public enum ErrorType {
UNKNOWN
}

public static ErrorType classify(String errorMessage) {
public static ErrorType classify(Throwable throwable) {
String errorMessage = buildFullErrorMessage(throwable);
if (errorMessage == null || errorMessage.isEmpty()) return ErrorType.UNKNOWN;
String lower = errorMessage.toLowerCase();

Expand All @@ -27,28 +28,64 @@ public static ErrorType classify(String errorMessage) {
lower.contains("driver version is not compatible") ||
lower.contains("unsupported protocol version") ||
lower.contains("no matching authentication protocol") ||
lower.contains("abstractmethoderror"))
lower.contains("abstractmethoderror") ||
lower.contains("the driver has not received any packets") ||
lower.contains("database not found") ||
lower.contains("catalog error") ||
lower.contains("the driver could not establish a secure connection"))
return ErrorType.VERSION_INCOMPATIBLE;

// Check auth patterns
if (lower.contains("ora-01017") || lower.contains("ora-01045") ||
lower.contains("invalid username/password") || lower.contains("login denied") ||
lower.contains("sql30082n") || lower.contains("wrong user name or password") ||
lower.contains("access denied") || lower.contains("authentication failed") ||
lower.contains("password incorrect") || lower.contains("password does not match"))
lower.contains("password incorrect") || lower.contains("password does not match") ||
lower.contains("login failed for user") ||
lower.contains("password authentication failed") ||
lower.contains("no pg_hba.conf entry") ||
lower.contains("incorrect username or password") ||
lower.contains("18456") ||
lower.contains("18470") ||
lower.contains("cannot open database") ||
lower.contains("using password") ||
lower.contains("login failed") ||
lower.contains("authentication violation"))
return ErrorType.AUTHENTICATION_FAILED;

// Check network patterns
if (lower.contains("connection refused") || lower.contains("connection reset") ||
lower.contains("unknown host") || lower.contains("unreachable") ||
lower.contains("network adapter") || lower.contains("connection closed") ||
lower.contains("communications link"))
lower.contains("communications link") ||
lower.contains("communicationsexception") ||
lower.contains("unable to connect") ||
lower.contains("could not create connection") ||
lower.contains("connection rejected") ||
lower.contains("connection error") ||
lower.contains("unable to connect to snowflake"))
return ErrorType.NETWORK_ERROR;

// Check timeout
if (lower.contains("timed out") || lower.contains("timeout"))
if (lower.contains("timed out") || lower.contains("timeout") ||
lower.contains("connection timed out") ||
lower.contains("connect timed out"))
return ErrorType.TIMEOUT;

return ErrorType.UNKNOWN;
}

private static String buildFullErrorMessage(Throwable throwable) {
if (throwable == null) return "";
StringBuilder sb = new StringBuilder();
Throwable current = throwable;
while (current != null) {
if (current.getMessage() != null) {
if (sb.length() > 0) sb.append(" | ");
sb.append(current.getMessage());
}
current = current.getCause();
}
return sb.toString();
}
}
21 changes: 19 additions & 2 deletions jdbc-bridge/src/main/java/sqlkit/bridge/ProtocolHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,31 @@ public ObjectNode handle(JsonNode request) {
} catch (ClassifiedException e) {
ObjectNode errorResp = MAPPER.createObjectNode();
errorResp.put("id", id);
errorResp.put("error", e.getMessage());
errorResp.put("error", buildErrorMessage(e));
errorResp.put("error_type", e.getErrorType().name().toLowerCase());
return errorResp;
} catch (Exception e) {
ObjectNode errorResp = MAPPER.createObjectNode();
errorResp.put("id", id);
errorResp.put("error", e.getMessage());
errorResp.put("error", buildErrorMessage(e));
return errorResp;
}
}

private static String buildErrorMessage(Throwable e) {
StringBuilder sb = new StringBuilder();
Throwable current = e;
while (current != null) {
if (sb.length() > 0) {
sb.append("\nCaused by: ");
}
String msg = current.getMessage();
sb.append(msg != null ? msg : current.getClass().getName());
current = current.getCause();
}
return sb.toString();
}

private void handleConnect(JsonNode params, ObjectNode response) throws Exception {
String connId = requiredString(params, "conn_id",
UUID.randomUUID().toString());
Expand All @@ -115,6 +129,9 @@ private void handleConnect(JsonNode params, ObjectNode response) throws Exceptio
}
}

// Extract Oracle-specific connection options
String tnsAdminDir = null;
String walletPassword = null;
connectionManager.connect(connId, url, username, password, driverClass, driverJars, poolMin, poolMax);
response.put("result", connId);
}
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
},
"devDependencies": {
"@antfu/eslint-config": "^7.0.1",
"@iconify-json/carbon": "^1.2.23",
"@iconify/json": "^2.2.482",
"@tauri-apps/cli": "^2",
"@types/dompurify": "^3.0.5",
Expand Down
3 changes: 2 additions & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ calamine = "0.22"
reqwest = { version = "0.12", default-features = false, features = [
"json",
"rustls-tls",
"socks"
"socks",
"stream"
] }
http = "1"
log = "0.4"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/capabilities/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ fn to_metadata(cap: &super::Capability) -> Value {

async fn resolve_connection_config(app: &AppHandle, connection_id: &str) -> Result<Value, String> {
let state: tauri::State<'_, crate::state::AppState> = app.state();
let conns = state.connections.lock().await;
let conns = state.connections.read().await;
let _active = conns
.get(connection_id)
.ok_or_else(|| format!("Connection not found: {}", connection_id))?;
Expand Down
33 changes: 29 additions & 4 deletions src-tauri/src/capabilities/sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async fn resolve_adapter(connection_id: &str) -> Result<ActiveConnection, String
// Check if already connected
{
let state: tauri::State<'_, crate::state::AppState> = app.state();
let conns = state.connections.lock().await;
let conns = state.connections.read().await;
if let Some(adapter) = conns.get(connection_id) {
return Ok(adapter.clone());
}
Expand Down Expand Up @@ -63,7 +63,7 @@ async fn resolve_adapter(connection_id: &str) -> Result<ActiveConnection, String
// Store the adapter for future use
{
let state: tauri::State<'_, crate::state::AppState> = app.state();
let mut conns = state.connections.lock().await;
let mut conns = state.connections.write().await;
conns.insert(connection_id.to_string(), adapter.clone());
}

Expand Down Expand Up @@ -137,9 +137,34 @@ impl CapabilityHandler for ExecuteQueryHandler {
.and_then(|v| v.as_str())
.ok_or_else(|| "Missing 'sql' argument".to_string())?;
let adapter = resolve_adapter(&conn_id).await?;

// Check connection quality and warn the AI agent about flaky connections
let mut guardian_warning: Option<String> = None;
if let Some(guardian) = crate::GUARDIAN.get() {
if let Some(quality) = guardian.quality_score(&conn_id).await {
if quality.score < 50.0 {
guardian_warning = Some(format!(
"Connection quality is low (score: {:.0}/100). \
Error count: {}, avg latency: {:.0}ms. \
The agent should be cautious about flaky connections.",
quality.score, quality.error_count, quality.avg_latency_ms
));
}
}
}

let result = execute_on_adapter(&adapter, sql).await?;
let json = serde_json::to_string(&result).map_err(|e| e.to_string())?;
Ok(crate::common::format::truncate_tool_output(json))
let mut response_map = serde_json::Map::new();
let json_val = serde_json::to_value(&result).map_err(|e| e.to_string())?;
response_map.insert("data".to_string(), json_val);
if let Some(warning) = guardian_warning {
response_map.insert(
"guardian_warning".to_string(),
serde_json::Value::String(warning),
);
}
let output = serde_json::to_string(&response_map).map_err(|e| e.to_string())?;
Ok(crate::common::format::truncate_tool_output(output))
}
}

Expand Down
Loading
Loading