Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
81 changes: 81 additions & 0 deletions jdbc-bridge/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.sqlkit</groupId>
<artifactId>jdbc-bridge</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<name>SQLKit JDBC Bridge</name>
<description>Lightweight JDBC proxy for SQLKit — connects to databases via JDBC and exposes JSON-RPC over stdin/stdout</description>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hikaricp.version>5.1.0</hikaricp.version>
<jackson.version>2.17.2</jackson.version>
<logback.version>1.5.7</logback.version>
</properties>

<dependencies>
<!-- HikariCP connection pool -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${hikaricp.version}</version>
</dependency>

<!-- Jackson for JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>

<!-- Logging -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
</dependencies>

<build>
<plugins>
<!-- Build fat JAR with all dependencies -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>sqlkit.bridge.BridgeMain</mainClass>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
50 changes: 50 additions & 0 deletions jdbc-bridge/src/main/java/sqlkit/bridge/BridgeMain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package sqlkit.bridge;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.io.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* BridgeMain — entry point for the SQLKit JDBC bridge.
*
* Reads JSON-RPC requests from stdin, dispatches them, and writes responses to stdout.
* Each line on stdin is a complete JSON request. Each response is a single JSON line on stdout.
*/
public class BridgeMain {

private static final ObjectMapper MAPPER = new ObjectMapper();
private static final Map<String, ConnectionManager> CONNECTIONS = new ConcurrentHashMap<>();
private static final ProtocolHandler HANDLER = new ProtocolHandler(CONNECTIONS);

public static void main(String[] args) throws Exception {
// Disable Jackson's FAIL_ON_EMPTY_BEANS for safety
MAPPER.disable(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
PrintWriter writer = new PrintWriter(new OutputStreamWriter(System.out));

String line;
while ((line = reader.readLine()) != null) {
if (line.trim().isEmpty()) {
continue;
}
try {
JsonNode request = MAPPER.readTree(line);
JsonNode response = HANDLER.handle(request);
writer.println(MAPPER.writeValueAsString(response));
writer.flush();
} catch (Exception e) {
// Send error response
ObjectNode errorResp = MAPPER.createObjectNode();
errorResp.put("id", -1);
errorResp.put("error", "Internal error: " + e.getMessage());
writer.println(MAPPER.writeValueAsString(errorResp));
writer.flush();
}
}
}
}
117 changes: 117 additions & 0 deletions jdbc-bridge/src/main/java/sqlkit/bridge/ConnectionManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package sqlkit.bridge;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.*;
import java.util.*;

/**
* Manages HikariCP connection pools for JDBC bridge connections.
* Each connection is identified by a unique conn_id string.
*/
public class ConnectionManager {

private final Map<String, HikariDataSource> pools = new HashMap<>();

/**
* Create a new JDBC connection pool.
*
* @param connId unique identifier for this connection
* @param url JDBC URL
* @param username database username
* @param password database password
* @param driverClass JDBC driver class name
* @param minPool minimum pool size
* @param maxPool maximum pool size
*/
public void connect(String connId, String url, String username,
String password, String driverClass,
int minPool, int maxPool) throws Exception {
if (pools.containsKey(connId)) {
throw new Exception("Connection already exists: " + connId);
}

// Load the JDBC driver class
Class.forName(driverClass);

HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
if (password != null && !password.isEmpty()) {
config.setPassword(password);
}
config.setMinimumIdle(minPool);
config.setMaximumPoolSize(maxPool);
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);

// Verify connection works
try (Connection c = ds.getConnection()) {
// ok
} catch (Exception e) {
ds.close();
throw new Exception("Failed to verify connection: " + e.getMessage());
}

pools.put(connId, ds);
}

/**
* Close and remove a connection pool.
*/
public void disconnect(String connId) {
HikariDataSource ds = pools.remove(connId);
if (ds != null) {
ds.close();
}
}

/**
* Get a connection from the pool for the given connId.
*/
public Connection getConnection(String connId) throws Exception {
HikariDataSource ds = pools.get(connId);
if (ds == null) {
throw new Exception("Connection not found: " + connId);
}
return ds.getConnection();
}

/**
* Test a connection — return status metadata as a Map.
*/
public Map<String, Object> testConnection(String connId) throws Exception {
try (Connection c = getConnection(connId)) {
Map<String, Object> status = new LinkedHashMap<>();
status.put("is_connected", true);

DatabaseMetaData meta = c.getMetaData();
status.put("server_version", meta.getDatabaseProductVersion());
status.put("current_database", c.getCatalog());
try {
status.put("current_user", meta.getUserName());
} catch (Exception e) {
status.put("current_user", null);
}

return status;
}
}

/**
* Close all connection pools.
*/
public void closeAll() {
for (HikariDataSource ds : pools.values()) {
ds.close();
}
pools.clear();
}
}
104 changes: 104 additions & 0 deletions jdbc-bridge/src/main/java/sqlkit/bridge/MetadataProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package sqlkit.bridge;

import java.sql.*;
import java.util.*;

/**
* Provides database metadata: databases, schemas, tables, columns.
*/
public class MetadataProvider {

/**
* List all databases (catalogs) on the server.
*/
public static List<String> listDatabases(Connection conn) throws Exception {
List<String> databases = new ArrayList<>();
try (ResultSet rs = conn.getMetaData().getCatalogs()) {
while (rs.next()) {
databases.add(rs.getString("TABLE_CAT"));
}
}
return databases;
}

/**
* List all schemas in the given database (catalog).
*/
public static List<String> listSchemas(Connection conn, String database) throws Exception {
List<String> schemas = new ArrayList<>();
String catalog = (database != null && !database.isEmpty()) ? database : null;
try (ResultSet rs = conn.getMetaData().getSchemas(catalog, null)) {
while (rs.next()) {
schemas.add(rs.getString("TABLE_SCHEM"));
}
}
return schemas;
}

/**
* List all tables in the given catalog/schema.
*/
public static List<Map<String, Object>> listTables(Connection conn,
String database,
String schema) throws Exception {
List<Map<String, Object>> tables = new ArrayList<>();
String catalog = (database != null && !database.isEmpty()) ? database : null;
String schemaPattern = (schema != null && !schema.isEmpty()) ? schema : null;

try (ResultSet rs = conn.getMetaData().getTables(catalog, schemaPattern, null,
new String[]{"TABLE", "VIEW", "SYSTEM TABLE", "ALIAS", "SYNONYM"})) {
while (rs.next()) {
Map<String, Object> t = new LinkedHashMap<>();
t.put("name", rs.getString("TABLE_NAME"));
t.put("schema", rs.getString("TABLE_SCHEM"));
t.put("table_type", rs.getString("TABLE_TYPE"));
t.put("row_count", null);
tables.add(t);
}
}
return tables;
}

/**
* List all columns for a given table.
*/
public static List<Map<String, Object>> listColumns(Connection conn,
String database,
String schema,
String table) throws Exception {
List<Map<String, Object>> columns = new ArrayList<>();
String catalog = (database != null && !database.isEmpty()) ? database : null;
String schemaPattern = (schema != null && !schema.isEmpty()) ? schema : null;

try (ResultSet rs = conn.getMetaData().getColumns(catalog, schemaPattern, table, null)) {
while (rs.next()) {
Map<String, Object> c = new LinkedHashMap<>();
c.put("name", rs.getString("COLUMN_NAME"));
c.put("data_type", rs.getString("TYPE_NAME"));
c.put("nullable", rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable);
c.put("default_value", rs.getString("COLUMN_DEF"));
c.put("is_primary_key", false); // filled below
c.put("is_auto_increment", "YES".equalsIgnoreCase(rs.getString("IS_AUTOINCREMENT")));
c.put("max_length", rs.getInt("COLUMN_SIZE"));
c.put("precision", rs.getInt("DECIMAL_DIGITS"));
c.put("scale", rs.getInt("DECIMAL_DIGITS"));
columns.add(c);
}
}

// Fetch primary keys for this table to set is_primary_key
Set<String> pkColumns = new HashSet<>();
try (ResultSet rs = conn.getMetaData().getPrimaryKeys(catalog, schemaPattern, table)) {
while (rs.next()) {
pkColumns.add(rs.getString("COLUMN_NAME"));
}
}
for (Map<String, Object> c : columns) {
if (pkColumns.contains(c.get("name"))) {
c.put("is_primary_key", true);
}
}

return columns;
}
}
Loading
Loading