16 changed files with 5477 additions and 0 deletions
-
338jdbc-driver/README.md
-
308jdbc-driver/examples/SeaweedFSJDBCExample.java
-
154jdbc-driver/pom.xml
-
497jdbc-driver/src/main/java/com/seaweedfs/jdbc/SeaweedFSConnection.java
-
71jdbc-driver/src/main/java/com/seaweedfs/jdbc/SeaweedFSConnectionInfo.java
-
972jdbc-driver/src/main/java/com/seaweedfs/jdbc/SeaweedFSDatabaseMetaData.java
-
207jdbc-driver/src/main/java/com/seaweedfs/jdbc/SeaweedFSDriver.java
-
352jdbc-driver/src/main/java/com/seaweedfs/jdbc/SeaweedFSPreparedStatement.java
-
1245jdbc-driver/src/main/java/com/seaweedfs/jdbc/SeaweedFSResultSet.java
-
202jdbc-driver/src/main/java/com/seaweedfs/jdbc/SeaweedFSResultSetMetaData.java
-
389jdbc-driver/src/main/java/com/seaweedfs/jdbc/SeaweedFSStatement.java
-
1jdbc-driver/src/main/resources/META-INF/services/java.sql.Driver
-
75jdbc-driver/src/test/java/com/seaweedfs/jdbc/SeaweedFSDriverTest.java
-
1weed/command/command.go
-
141weed/command/jdbc.go
-
524weed/server/jdbc_server.go
@ -0,0 +1,338 @@ |
|||
# SeaweedFS JDBC Driver |
|||
|
|||
A JDBC driver for connecting to SeaweedFS SQL engine, enabling standard Java applications and BI tools to query SeaweedFS MQ topics using SQL. |
|||
|
|||
## Features |
|||
|
|||
- **Standard JDBC Interface**: Compatible with any Java application or tool that supports JDBC |
|||
- **SQL Query Support**: Execute SELECT queries on SeaweedFS MQ topics |
|||
- **Aggregation Functions**: Support for COUNT, SUM, AVG, MIN, MAX operations |
|||
- **System Columns**: Access to `_timestamp_ns`, `_key`, `_source` system columns |
|||
- **Database Tools**: Works with DBeaver, IntelliJ DataGrip, and other database tools |
|||
- **BI Tools**: Compatible with Tableau, Power BI, and other business intelligence tools |
|||
- **Read-Only Access**: Secure read-only access to your SeaweedFS data |
|||
|
|||
## Quick Start |
|||
|
|||
### 1. Start SeaweedFS JDBC Server |
|||
|
|||
First, start the SeaweedFS JDBC server: |
|||
|
|||
```bash |
|||
# Start JDBC server on default port 8089 |
|||
weed jdbc |
|||
|
|||
# Or with custom configuration |
|||
weed jdbc -port=8090 -host=0.0.0.0 -master=master-server:9333 |
|||
``` |
|||
|
|||
### 2. Add JDBC Driver to Your Project |
|||
|
|||
#### Maven |
|||
|
|||
```xml |
|||
<dependency> |
|||
<groupId>com.seaweedfs</groupId> |
|||
<artifactId>seaweedfs-jdbc</artifactId> |
|||
<version>1.0.0</version> |
|||
</dependency> |
|||
``` |
|||
|
|||
#### Gradle |
|||
|
|||
```gradle |
|||
implementation 'com.seaweedfs:seaweedfs-jdbc:1.0.0' |
|||
``` |
|||
|
|||
### 3. Connect and Query |
|||
|
|||
```java |
|||
import java.sql.*; |
|||
|
|||
public class SeaweedFSExample { |
|||
public static void main(String[] args) throws SQLException { |
|||
// JDBC URL format: jdbc:seaweedfs://host:port/database |
|||
String url = "jdbc:seaweedfs://localhost:8089/default"; |
|||
|
|||
// Connect to SeaweedFS |
|||
Connection conn = DriverManager.getConnection(url); |
|||
|
|||
// Execute queries |
|||
Statement stmt = conn.createStatement(); |
|||
ResultSet rs = stmt.executeQuery("SELECT * FROM my_topic LIMIT 10"); |
|||
|
|||
// Process results |
|||
while (rs.next()) { |
|||
System.out.println("ID: " + rs.getLong("id")); |
|||
System.out.println("Message: " + rs.getString("message")); |
|||
System.out.println("Timestamp: " + rs.getTimestamp("_timestamp_ns")); |
|||
} |
|||
|
|||
// Clean up |
|||
rs.close(); |
|||
stmt.close(); |
|||
conn.close(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## JDBC URL Format |
|||
|
|||
``` |
|||
jdbc:seaweedfs://host:port/database[?property=value&...] |
|||
``` |
|||
|
|||
### Parameters |
|||
|
|||
| Parameter | Default | Description | |
|||
|-----------|---------|-------------| |
|||
| `host` | localhost | SeaweedFS JDBC server hostname | |
|||
| `port` | 8089 | SeaweedFS JDBC server port | |
|||
| `database` | default | Database/namespace name | |
|||
| `connectTimeout` | 30000 | Connection timeout in milliseconds | |
|||
| `socketTimeout` | 0 | Socket timeout in milliseconds (0 = infinite) | |
|||
|
|||
### Examples |
|||
|
|||
```java |
|||
// Basic connection |
|||
"jdbc:seaweedfs://localhost:8089/default" |
|||
|
|||
// Custom host and port |
|||
"jdbc:seaweedfs://seaweed-server:9000/production" |
|||
|
|||
// With query parameters |
|||
"jdbc:seaweedfs://localhost:8089/default?connectTimeout=5000&socketTimeout=30000" |
|||
``` |
|||
|
|||
## Supported SQL Operations |
|||
|
|||
### SELECT Queries |
|||
```sql |
|||
-- Basic select |
|||
SELECT * FROM topic_name; |
|||
|
|||
-- With WHERE clause |
|||
SELECT id, message FROM topic_name WHERE id > 1000; |
|||
|
|||
-- With LIMIT |
|||
SELECT * FROM topic_name ORDER BY _timestamp_ns DESC LIMIT 100; |
|||
``` |
|||
|
|||
### Aggregation Functions |
|||
```sql |
|||
-- Count records |
|||
SELECT COUNT(*) FROM topic_name; |
|||
|
|||
-- Aggregations |
|||
SELECT |
|||
COUNT(*) as total_messages, |
|||
MIN(id) as min_id, |
|||
MAX(id) as max_id, |
|||
AVG(amount) as avg_amount |
|||
FROM topic_name; |
|||
``` |
|||
|
|||
### System Columns |
|||
```sql |
|||
-- Access system columns |
|||
SELECT |
|||
id, |
|||
message, |
|||
_timestamp_ns as timestamp, |
|||
_key as partition_key, |
|||
_source as data_source |
|||
FROM topic_name; |
|||
``` |
|||
|
|||
### Schema Information |
|||
```sql |
|||
-- List databases |
|||
SHOW DATABASES; |
|||
|
|||
-- List tables in current database |
|||
SHOW TABLES; |
|||
|
|||
-- Describe table structure |
|||
DESCRIBE topic_name; |
|||
-- or |
|||
DESC topic_name; |
|||
``` |
|||
|
|||
## Database Tool Integration |
|||
|
|||
### DBeaver |
|||
|
|||
1. Download and install DBeaver |
|||
2. Create new connection → Generic JDBC |
|||
3. Settings: |
|||
- **URL**: `jdbc:seaweedfs://localhost:8089/default` |
|||
- **Driver Class**: `com.seaweedfs.jdbc.SeaweedFSDriver` |
|||
- **Libraries**: Add `seaweedfs-jdbc-1.0.0.jar` |
|||
|
|||
### IntelliJ DataGrip |
|||
|
|||
1. Open DataGrip |
|||
2. Add New Data Source → Generic |
|||
3. Configure: |
|||
- **URL**: `jdbc:seaweedfs://localhost:8089/default` |
|||
- **Driver**: Add `seaweedfs-jdbc-1.0.0.jar` |
|||
- **Driver Class**: `com.seaweedfs.jdbc.SeaweedFSDriver` |
|||
|
|||
### Tableau |
|||
|
|||
1. Connect to Data → More... → Generic JDBC |
|||
2. Configure: |
|||
- **URL**: `jdbc:seaweedfs://localhost:8089/default` |
|||
- **Driver Path**: Path to `seaweedfs-jdbc-1.0.0.jar` |
|||
- **Class Name**: `com.seaweedfs.jdbc.SeaweedFSDriver` |
|||
|
|||
## Advanced Usage |
|||
|
|||
### Connection Pooling |
|||
|
|||
```java |
|||
import com.zaxxer.hikari.HikariConfig; |
|||
import com.zaxxer.hikari.HikariDataSource; |
|||
|
|||
HikariConfig config = new HikariConfig(); |
|||
config.setJdbcUrl("jdbc:seaweedfs://localhost:8089/default"); |
|||
config.setMaximumPoolSize(10); |
|||
|
|||
HikariDataSource dataSource = new HikariDataSource(config); |
|||
Connection conn = dataSource.getConnection(); |
|||
``` |
|||
|
|||
### PreparedStatements |
|||
|
|||
```java |
|||
String sql = "SELECT * FROM topic_name WHERE id > ? AND created_date > ?"; |
|||
PreparedStatement stmt = conn.prepareStatement(sql); |
|||
stmt.setLong(1, 1000); |
|||
stmt.setTimestamp(2, Timestamp.valueOf("2024-01-01 00:00:00")); |
|||
|
|||
ResultSet rs = stmt.executeQuery(); |
|||
while (rs.next()) { |
|||
// Process results |
|||
} |
|||
``` |
|||
|
|||
### Metadata Access |
|||
|
|||
```java |
|||
DatabaseMetaData metadata = conn.getMetaData(); |
|||
|
|||
// Get database information |
|||
System.out.println("Database: " + metadata.getDatabaseProductName()); |
|||
System.out.println("Version: " + metadata.getDatabaseProductVersion()); |
|||
System.out.println("Driver: " + metadata.getDriverName()); |
|||
|
|||
// Get table information |
|||
ResultSet tables = metadata.getTables(null, null, null, null); |
|||
while (tables.next()) { |
|||
System.out.println("Table: " + tables.getString("TABLE_NAME")); |
|||
} |
|||
``` |
|||
|
|||
## Building from Source |
|||
|
|||
```bash |
|||
# Clone the repository |
|||
git clone https://github.com/seaweedfs/seaweedfs.git |
|||
cd seaweedfs/jdbc-driver |
|||
|
|||
# Build with Maven |
|||
mvn clean package |
|||
|
|||
# Run tests |
|||
mvn test |
|||
|
|||
# Install to local repository |
|||
mvn install |
|||
``` |
|||
|
|||
## Configuration |
|||
|
|||
### Server-Side Configuration |
|||
|
|||
The JDBC server supports the following command-line options: |
|||
|
|||
```bash |
|||
weed jdbc -help |
|||
-host string |
|||
JDBC server host (default "localhost") |
|||
-master string |
|||
SeaweedFS master server address (default "localhost:9333") |
|||
-port int |
|||
JDBC server port (default 8089) |
|||
``` |
|||
|
|||
### Client-Side Configuration |
|||
|
|||
Connection properties can be set via URL parameters or Properties object: |
|||
|
|||
```java |
|||
Properties props = new Properties(); |
|||
props.setProperty("connectTimeout", "10000"); |
|||
props.setProperty("socketTimeout", "30000"); |
|||
|
|||
Connection conn = DriverManager.getConnection( |
|||
"jdbc:seaweedfs://localhost:8089/default", props); |
|||
``` |
|||
|
|||
## Performance Tips |
|||
|
|||
1. **Use LIMIT clauses**: Always limit result sets for large topics |
|||
2. **Filter early**: Use WHERE clauses to reduce data transfer |
|||
3. **Connection pooling**: Use connection pools for multi-threaded applications |
|||
4. **Batch operations**: Use batch statements for multiple queries |
|||
5. **Close resources**: Always close ResultSets, Statements, and Connections |
|||
|
|||
## Limitations |
|||
|
|||
- **Read-Only**: SeaweedFS JDBC driver only supports SELECT operations |
|||
- **No Transactions**: Transaction support is not available |
|||
- **Single Table**: Joins between tables are not supported |
|||
- **Limited SQL**: Only basic SQL SELECT syntax is supported |
|||
|
|||
## Troubleshooting |
|||
|
|||
### Connection Issues |
|||
|
|||
```bash |
|||
# Test JDBC server connectivity |
|||
telnet localhost 8089 |
|||
|
|||
# Check SeaweedFS master connectivity |
|||
weed shell |
|||
> cluster.status |
|||
``` |
|||
|
|||
### Common Errors |
|||
|
|||
**Error: "Connection refused"** |
|||
- Ensure JDBC server is running on the specified host/port |
|||
- Check firewall settings |
|||
|
|||
**Error: "No suitable driver found"** |
|||
- Verify JDBC driver is in classpath |
|||
- Ensure correct driver class name: `com.seaweedfs.jdbc.SeaweedFSDriver` |
|||
|
|||
**Error: "Topic not found"** |
|||
- Verify topic exists in SeaweedFS |
|||
- Check database/namespace name in connection URL |
|||
|
|||
## Contributing |
|||
|
|||
Contributions are welcome! Please see the main SeaweedFS repository for contribution guidelines. |
|||
|
|||
## License |
|||
|
|||
This JDBC driver is part of SeaweedFS and is licensed under the Apache License 2.0. |
|||
|
|||
## Support |
|||
|
|||
- **Documentation**: [SeaweedFS Wiki](https://github.com/seaweedfs/seaweedfs/wiki) |
|||
- **Issues**: [GitHub Issues](https://github.com/seaweedfs/seaweedfs/issues) |
|||
- **Discussions**: [GitHub Discussions](https://github.com/seaweedfs/seaweedfs/discussions) |
|||
- **Chat**: [SeaweedFS Slack](https://join.slack.com/t/seaweedfs/shared_invite/...) |
@ -0,0 +1,308 @@ |
|||
package com.seaweedfs.jdbc.examples; |
|||
|
|||
import java.sql.*; |
|||
import java.util.Properties; |
|||
|
|||
/** |
|||
* Complete example demonstrating SeaweedFS JDBC driver usage |
|||
*/ |
|||
public class SeaweedFSJDBCExample { |
|||
|
|||
public static void main(String[] args) { |
|||
// JDBC URL for SeaweedFS |
|||
String url = "jdbc:seaweedfs://localhost:8089/default"; |
|||
|
|||
try { |
|||
// 1. Load the driver (optional - auto-registration via META-INF/services) |
|||
Class.forName("com.seaweedfs.jdbc.SeaweedFSDriver"); |
|||
System.out.println("✓ SeaweedFS JDBC Driver loaded successfully"); |
|||
|
|||
// 2. Connect to SeaweedFS |
|||
System.out.println("\n📡 Connecting to SeaweedFS..."); |
|||
Connection conn = DriverManager.getConnection(url); |
|||
System.out.println("✓ Connected to: " + url); |
|||
|
|||
// 3. Get database metadata |
|||
DatabaseMetaData dbMeta = conn.getMetaData(); |
|||
System.out.println("\n📊 Database Information:"); |
|||
System.out.println(" Database: " + dbMeta.getDatabaseProductName()); |
|||
System.out.println(" Version: " + dbMeta.getDatabaseProductVersion()); |
|||
System.out.println(" Driver: " + dbMeta.getDriverName() + " v" + dbMeta.getDriverVersion()); |
|||
System.out.println(" JDBC Version: " + dbMeta.getJDBCMajorVersion() + "." + dbMeta.getJDBCMinorVersion()); |
|||
System.out.println(" Read-only: " + dbMeta.isReadOnly()); |
|||
|
|||
// 4. List available databases/schemas |
|||
System.out.println("\n🗄️ Available Databases:"); |
|||
ResultSet catalogs = dbMeta.getCatalogs(); |
|||
while (catalogs.next()) { |
|||
System.out.println(" • " + catalogs.getString("TABLE_CAT")); |
|||
} |
|||
catalogs.close(); |
|||
|
|||
// 5. Execute basic queries |
|||
System.out.println("\n🔍 Executing SQL Queries:"); |
|||
|
|||
Statement stmt = conn.createStatement(); |
|||
|
|||
// Show databases |
|||
System.out.println("\n 📋 SHOW DATABASES:"); |
|||
ResultSet rs = stmt.executeQuery("SHOW DATABASES"); |
|||
while (rs.next()) { |
|||
System.out.println(" " + rs.getString(1)); |
|||
} |
|||
rs.close(); |
|||
|
|||
// Show tables (topics) |
|||
System.out.println("\n 📋 SHOW TABLES:"); |
|||
rs = stmt.executeQuery("SHOW TABLES"); |
|||
ResultSetMetaData rsmd = rs.getMetaData(); |
|||
int columnCount = rsmd.getColumnCount(); |
|||
|
|||
// Print headers |
|||
for (int i = 1; i <= columnCount; i++) { |
|||
System.out.print(String.format("%-20s", rsmd.getColumnName(i))); |
|||
} |
|||
System.out.println(); |
|||
System.out.println("-".repeat(20 * columnCount)); |
|||
|
|||
// Print rows |
|||
while (rs.next()) { |
|||
for (int i = 1; i <= columnCount; i++) { |
|||
System.out.print(String.format("%-20s", rs.getString(i))); |
|||
} |
|||
System.out.println(); |
|||
} |
|||
rs.close(); |
|||
|
|||
// 6. Query a specific topic (if exists) |
|||
String topicQuery = "SELECT * FROM test_topic LIMIT 5"; |
|||
System.out.println("\n 📋 " + topicQuery + ":"); |
|||
|
|||
try { |
|||
rs = stmt.executeQuery(topicQuery); |
|||
rsmd = rs.getMetaData(); |
|||
columnCount = rsmd.getColumnCount(); |
|||
|
|||
// Print column headers |
|||
for (int i = 1; i <= columnCount; i++) { |
|||
System.out.print(String.format("%-15s", rsmd.getColumnName(i))); |
|||
} |
|||
System.out.println(); |
|||
System.out.println("-".repeat(15 * columnCount)); |
|||
|
|||
// Print data rows |
|||
int rowCount = 0; |
|||
while (rs.next() && rowCount < 5) { |
|||
for (int i = 1; i <= columnCount; i++) { |
|||
String value = rs.getString(i); |
|||
if (value != null && value.length() > 12) { |
|||
value = value.substring(0, 12) + "..."; |
|||
} |
|||
System.out.print(String.format("%-15s", value != null ? value : "NULL")); |
|||
} |
|||
System.out.println(); |
|||
rowCount++; |
|||
} |
|||
|
|||
if (rowCount == 0) { |
|||
System.out.println(" (No data found)"); |
|||
} |
|||
|
|||
rs.close(); |
|||
} catch (SQLException e) { |
|||
System.out.println(" ⚠️ Topic 'test_topic' not found: " + e.getMessage()); |
|||
} |
|||
|
|||
// 7. Demonstrate aggregation queries |
|||
System.out.println("\n 🧮 Aggregation Example:"); |
|||
try { |
|||
rs = stmt.executeQuery("SELECT COUNT(*) as total_records FROM test_topic"); |
|||
if (rs.next()) { |
|||
System.out.println(" Total records: " + rs.getLong("total_records")); |
|||
} |
|||
rs.close(); |
|||
} catch (SQLException e) { |
|||
System.out.println(" ⚠️ Aggregation example skipped: " + e.getMessage()); |
|||
} |
|||
|
|||
// 8. Demonstrate PreparedStatement |
|||
System.out.println("\n 📝 PreparedStatement Example:"); |
|||
String preparedQuery = "SELECT * FROM test_topic WHERE id > ? LIMIT ?"; |
|||
|
|||
try { |
|||
PreparedStatement pstmt = conn.prepareStatement(preparedQuery); |
|||
pstmt.setLong(1, 100); |
|||
pstmt.setInt(2, 3); |
|||
|
|||
System.out.println(" Query: " + preparedQuery); |
|||
System.out.println(" Parameters: id > 100, LIMIT 3"); |
|||
|
|||
rs = pstmt.executeQuery(); |
|||
rsmd = rs.getMetaData(); |
|||
columnCount = rsmd.getColumnCount(); |
|||
|
|||
int count = 0; |
|||
while (rs.next()) { |
|||
if (count == 0) { |
|||
// Print headers for first row |
|||
for (int i = 1; i <= columnCount; i++) { |
|||
System.out.print(String.format("%-15s", rsmd.getColumnName(i))); |
|||
} |
|||
System.out.println(); |
|||
System.out.println("-".repeat(15 * columnCount)); |
|||
} |
|||
|
|||
for (int i = 1; i <= columnCount; i++) { |
|||
String value = rs.getString(i); |
|||
if (value != null && value.length() > 12) { |
|||
value = value.substring(0, 12) + "..."; |
|||
} |
|||
System.out.print(String.format("%-15s", value != null ? value : "NULL")); |
|||
} |
|||
System.out.println(); |
|||
count++; |
|||
} |
|||
|
|||
if (count == 0) { |
|||
System.out.println(" (No records match criteria)"); |
|||
} |
|||
|
|||
rs.close(); |
|||
pstmt.close(); |
|||
} catch (SQLException e) { |
|||
System.out.println(" ⚠️ PreparedStatement example skipped: " + e.getMessage()); |
|||
} |
|||
|
|||
// 9. System columns example |
|||
System.out.println("\n 🔧 System Columns Example:"); |
|||
try { |
|||
rs = stmt.executeQuery("SELECT id, _timestamp_ns, _key, _source FROM test_topic LIMIT 3"); |
|||
rsmd = rs.getMetaData(); |
|||
columnCount = rsmd.getColumnCount(); |
|||
|
|||
// Print headers |
|||
for (int i = 1; i <= columnCount; i++) { |
|||
System.out.print(String.format("%-20s", rsmd.getColumnName(i))); |
|||
} |
|||
System.out.println(); |
|||
System.out.println("-".repeat(20 * columnCount)); |
|||
|
|||
int count = 0; |
|||
while (rs.next()) { |
|||
for (int i = 1; i <= columnCount; i++) { |
|||
String value = rs.getString(i); |
|||
if (value != null && value.length() > 17) { |
|||
value = value.substring(0, 17) + "..."; |
|||
} |
|||
System.out.print(String.format("%-20s", value != null ? value : "NULL")); |
|||
} |
|||
System.out.println(); |
|||
count++; |
|||
} |
|||
|
|||
if (count == 0) { |
|||
System.out.println(" (No data available for system columns demo)"); |
|||
} |
|||
|
|||
rs.close(); |
|||
} catch (SQLException e) { |
|||
System.out.println(" ⚠️ System columns example skipped: " + e.getMessage()); |
|||
} |
|||
|
|||
// 10. Connection properties example |
|||
System.out.println("\n⚙️ Connection Properties:"); |
|||
System.out.println(" Auto-commit: " + conn.getAutoCommit()); |
|||
System.out.println(" Read-only: " + conn.isReadOnly()); |
|||
System.out.println(" Transaction isolation: " + conn.getTransactionIsolation()); |
|||
System.out.println(" Catalog: " + conn.getCatalog()); |
|||
|
|||
// 11. Clean up |
|||
stmt.close(); |
|||
conn.close(); |
|||
|
|||
System.out.println("\n✅ SeaweedFS JDBC Example completed successfully!"); |
|||
System.out.println("\n💡 Next Steps:"); |
|||
System.out.println(" • Try connecting with DBeaver or other JDBC tools"); |
|||
System.out.println(" • Use in your Java applications with connection pooling"); |
|||
System.out.println(" • Integrate with BI tools like Tableau or Power BI"); |
|||
System.out.println(" • Build data pipelines using SeaweedFS as a data source"); |
|||
|
|||
} catch (ClassNotFoundException e) { |
|||
System.err.println("❌ SeaweedFS JDBC Driver not found: " + e.getMessage()); |
|||
System.err.println(" Make sure seaweedfs-jdbc.jar is in your classpath"); |
|||
} catch (SQLException e) { |
|||
System.err.println("❌ Database error: " + e.getMessage()); |
|||
System.err.println(" Make sure SeaweedFS JDBC server is running:"); |
|||
System.err.println(" weed jdbc -port=8089 -master=localhost:9333"); |
|||
} catch (Exception e) { |
|||
System.err.println("❌ Unexpected error: " + e.getMessage()); |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example with connection pooling using HikariCP |
|||
*/ |
|||
public static void connectionPoolingExample() { |
|||
try { |
|||
// This would require HikariCP dependency |
|||
/* |
|||
HikariConfig config = new HikariConfig(); |
|||
config.setJdbcUrl("jdbc:seaweedfs://localhost:8089/default"); |
|||
config.setMaximumPoolSize(10); |
|||
config.setMinimumIdle(2); |
|||
config.setConnectionTimeout(30000); |
|||
config.setIdleTimeout(600000); |
|||
|
|||
HikariDataSource dataSource = new HikariDataSource(config); |
|||
|
|||
try (Connection conn = dataSource.getConnection()) { |
|||
// Use connection from pool |
|||
Statement stmt = conn.createStatement(); |
|||
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM my_topic"); |
|||
if (rs.next()) { |
|||
System.out.println("Record count: " + rs.getLong(1)); |
|||
} |
|||
rs.close(); |
|||
stmt.close(); |
|||
} |
|||
|
|||
dataSource.close(); |
|||
*/ |
|||
|
|||
System.out.println("Connection pooling example (commented out - requires HikariCP dependency)"); |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example configuration for different database tools |
|||
*/ |
|||
public static void printToolConfiguration() { |
|||
System.out.println("\n🛠️ Database Tool Configuration:"); |
|||
|
|||
System.out.println("\n📊 DBeaver:"); |
|||
System.out.println(" 1. New Connection → Generic JDBC"); |
|||
System.out.println(" 2. URL: jdbc:seaweedfs://localhost:8089/default"); |
|||
System.out.println(" 3. Driver Class: com.seaweedfs.jdbc.SeaweedFSDriver"); |
|||
System.out.println(" 4. Add seaweedfs-jdbc.jar to Libraries"); |
|||
|
|||
System.out.println("\n💻 IntelliJ DataGrip:"); |
|||
System.out.println(" 1. New Data Source → Generic"); |
|||
System.out.println(" 2. URL: jdbc:seaweedfs://localhost:8089/default"); |
|||
System.out.println(" 3. Add Driver: seaweedfs-jdbc.jar"); |
|||
System.out.println(" 4. Class: com.seaweedfs.jdbc.SeaweedFSDriver"); |
|||
|
|||
System.out.println("\n📈 Tableau:"); |
|||
System.out.println(" 1. Connect to Data → More... → Generic JDBC"); |
|||
System.out.println(" 2. URL: jdbc:seaweedfs://localhost:8089/default"); |
|||
System.out.println(" 3. Driver Path: /path/to/seaweedfs-jdbc.jar"); |
|||
System.out.println(" 4. Class Name: com.seaweedfs.jdbc.SeaweedFSDriver"); |
|||
|
|||
System.out.println("\n☕ Java Application:"); |
|||
System.out.println(" Class.forName(\"com.seaweedfs.jdbc.SeaweedFSDriver\");"); |
|||
System.out.println(" Connection conn = DriverManager.getConnection("); |
|||
System.out.println(" \"jdbc:seaweedfs://localhost:8089/default\");"); |
|||
} |
|||
} |
@ -0,0 +1,154 @@ |
|||
<?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.seaweedfs</groupId> |
|||
<artifactId>seaweedfs-jdbc</artifactId> |
|||
<version>1.0.0</version> |
|||
<packaging>jar</packaging> |
|||
|
|||
<name>SeaweedFS JDBC Driver</name> |
|||
<description>JDBC driver for connecting to SeaweedFS SQL engine</description> |
|||
<url>https://github.com/seaweedfs/seaweedfs</url> |
|||
|
|||
<licenses> |
|||
<license> |
|||
<name>Apache License, Version 2.0</name> |
|||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> |
|||
<distribution>repo</distribution> |
|||
</license> |
|||
</licenses> |
|||
|
|||
<properties> |
|||
<maven.compiler.source>8</maven.compiler.source> |
|||
<maven.compiler.target>8</maven.compiler.target> |
|||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
|||
<junit.version>5.9.0</junit.version> |
|||
</properties> |
|||
|
|||
<dependencies> |
|||
<!-- JUnit for testing --> |
|||
<dependency> |
|||
<groupId>org.junit.jupiter</groupId> |
|||
<artifactId>junit-jupiter-engine</artifactId> |
|||
<version>${junit.version}</version> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.junit.jupiter</groupId> |
|||
<artifactId>junit-jupiter-api</artifactId> |
|||
<version>${junit.version}</version> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
|
|||
<!-- SLF4J for logging --> |
|||
<dependency> |
|||
<groupId>org.slf4j</groupId> |
|||
<artifactId>slf4j-api</artifactId> |
|||
<version>1.7.36</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.slf4j</groupId> |
|||
<artifactId>slf4j-simple</artifactId> |
|||
<version>1.7.36</version> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
<build> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-compiler-plugin</artifactId> |
|||
<version>3.10.1</version> |
|||
<configuration> |
|||
<source>8</source> |
|||
<target>8</target> |
|||
<encoding>UTF-8</encoding> |
|||
</configuration> |
|||
</plugin> |
|||
|
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-surefire-plugin</artifactId> |
|||
<version>3.0.0-M7</version> |
|||
<configuration> |
|||
<useSystemClassLoader>false</useSystemClassLoader> |
|||
</configuration> |
|||
</plugin> |
|||
|
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-jar-plugin</artifactId> |
|||
<version>3.2.2</version> |
|||
<configuration> |
|||
<archive> |
|||
<manifestEntries> |
|||
<Automatic-Module-Name>com.seaweedfs.jdbc</Automatic-Module-Name> |
|||
</manifestEntries> |
|||
</archive> |
|||
</configuration> |
|||
</plugin> |
|||
|
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-source-plugin</artifactId> |
|||
<version>3.2.1</version> |
|||
<executions> |
|||
<execution> |
|||
<id>attach-sources</id> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
|
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-javadoc-plugin</artifactId> |
|||
<version>3.4.1</version> |
|||
<executions> |
|||
<execution> |
|||
<id>attach-javadocs</id> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
<configuration> |
|||
<source>8</source> |
|||
<target>8</target> |
|||
<doclint>none</doclint> |
|||
</configuration> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
|
|||
<profiles> |
|||
<profile> |
|||
<id>release</id> |
|||
<build> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-gpg-plugin</artifactId> |
|||
<version>3.0.1</version> |
|||
<executions> |
|||
<execution> |
|||
<id>sign-artifacts</id> |
|||
<phase>verify</phase> |
|||
<goals> |
|||
<goal>sign</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
</profile> |
|||
</profiles> |
|||
</project> |
@ -0,0 +1,497 @@ |
|||
package com.seaweedfs.jdbc; |
|||
|
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
|
|||
import java.io.*; |
|||
import java.net.Socket; |
|||
import java.net.SocketTimeoutException; |
|||
import java.nio.ByteBuffer; |
|||
import java.sql.*; |
|||
import java.util.Map; |
|||
import java.util.Properties; |
|||
import java.util.concurrent.Executor; |
|||
|
|||
/** |
|||
* JDBC Connection implementation for SeaweedFS |
|||
*/ |
|||
public class SeaweedFSConnection implements Connection { |
|||
|
|||
private static final Logger logger = LoggerFactory.getLogger(SeaweedFSConnection.class); |
|||
|
|||
// Protocol constants (must match server implementation) |
|||
private static final byte JDBC_MSG_CONNECT = 0x01; |
|||
private static final byte JDBC_MSG_DISCONNECT = 0x02; |
|||
private static final byte JDBC_MSG_EXECUTE_QUERY = 0x03; |
|||
private static final byte JDBC_MSG_EXECUTE_UPDATE = 0x04; |
|||
private static final byte JDBC_MSG_GET_METADATA = 0x07; |
|||
private static final byte JDBC_MSG_SET_AUTOCOMMIT = 0x08; |
|||
private static final byte JDBC_MSG_COMMIT = 0x09; |
|||
private static final byte JDBC_MSG_ROLLBACK = 0x0A; |
|||
|
|||
private static final byte JDBC_RESP_OK = 0x00; |
|||
private static final byte JDBC_RESP_ERROR = 0x01; |
|||
private static final byte JDBC_RESP_RESULT_SET = 0x02; |
|||
private static final byte JDBC_RESP_UPDATE_COUNT = 0x03; |
|||
private static final byte JDBC_RESP_METADATA = 0x04; |
|||
|
|||
private final SeaweedFSConnectionInfo connectionInfo; |
|||
private Socket socket; |
|||
private DataInputStream inputStream; |
|||
private DataOutputStream outputStream; |
|||
private boolean closed = false; |
|||
private boolean autoCommit = true; |
|||
private String catalog = null; |
|||
private int transactionIsolation = Connection.TRANSACTION_NONE; |
|||
private boolean readOnly = true; // SeaweedFS is read-only |
|||
|
|||
public SeaweedFSConnection(SeaweedFSConnectionInfo connectionInfo) throws SQLException { |
|||
this.connectionInfo = connectionInfo; |
|||
connect(); |
|||
} |
|||
|
|||
private void connect() throws SQLException { |
|||
try { |
|||
logger.debug("Connecting to SeaweedFS at {}:{}", connectionInfo.getHost(), connectionInfo.getPort()); |
|||
|
|||
// Create socket connection |
|||
socket = new Socket(); |
|||
socket.connect(new java.net.InetSocketAddress(connectionInfo.getHost(), connectionInfo.getPort()), |
|||
connectionInfo.getConnectTimeout()); |
|||
|
|||
if (connectionInfo.getSocketTimeout() > 0) { |
|||
socket.setSoTimeout(connectionInfo.getSocketTimeout()); |
|||
} |
|||
|
|||
// Create streams |
|||
inputStream = new DataInputStream(new BufferedInputStream(socket.getInputStream())); |
|||
outputStream = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream())); |
|||
|
|||
// Send connection message |
|||
sendMessage(JDBC_MSG_CONNECT, connectionInfo.getDatabase().getBytes()); |
|||
|
|||
// Read response |
|||
Response response = readResponse(); |
|||
if (response.type == JDBC_RESP_ERROR) { |
|||
throw new SQLException("Failed to connect: " + new String(response.data)); |
|||
} |
|||
|
|||
logger.info("Successfully connected to SeaweedFS: {}", connectionInfo.getConnectionString()); |
|||
|
|||
} catch (Exception e) { |
|||
if (socket != null && !socket.isClosed()) { |
|||
try { |
|||
socket.close(); |
|||
} catch (IOException ignored) {} |
|||
} |
|||
throw new SQLException("Failed to connect to SeaweedFS: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Statement createStatement() throws SQLException { |
|||
checkClosed(); |
|||
return new SeaweedFSStatement(this); |
|||
} |
|||
|
|||
@Override |
|||
public PreparedStatement prepareStatement(String sql) throws SQLException { |
|||
checkClosed(); |
|||
return new SeaweedFSPreparedStatement(this, sql); |
|||
} |
|||
|
|||
@Override |
|||
public CallableStatement prepareCall(String sql) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Callable statements are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public String nativeSQL(String sql) throws SQLException { |
|||
checkClosed(); |
|||
return sql; // No translation needed |
|||
} |
|||
|
|||
@Override |
|||
public void setAutoCommit(boolean autoCommit) throws SQLException { |
|||
checkClosed(); |
|||
if (this.autoCommit != autoCommit) { |
|||
sendMessage(JDBC_MSG_SET_AUTOCOMMIT, new byte[]{(byte)(autoCommit ? 1 : 0)}); |
|||
Response response = readResponse(); |
|||
if (response.type == JDBC_RESP_ERROR) { |
|||
throw new SQLException("Failed to set auto-commit: " + new String(response.data)); |
|||
} |
|||
this.autoCommit = autoCommit; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public boolean getAutoCommit() throws SQLException { |
|||
checkClosed(); |
|||
return autoCommit; |
|||
} |
|||
|
|||
@Override |
|||
public void commit() throws SQLException { |
|||
checkClosed(); |
|||
if (autoCommit) { |
|||
throw new SQLException("Cannot commit when auto-commit is enabled"); |
|||
} |
|||
sendMessage(JDBC_MSG_COMMIT, new byte[0]); |
|||
Response response = readResponse(); |
|||
if (response.type == JDBC_RESP_ERROR) { |
|||
throw new SQLException("Failed to commit: " + new String(response.data)); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void rollback() throws SQLException { |
|||
checkClosed(); |
|||
if (autoCommit) { |
|||
throw new SQLException("Cannot rollback when auto-commit is enabled"); |
|||
} |
|||
sendMessage(JDBC_MSG_ROLLBACK, new byte[0]); |
|||
Response response = readResponse(); |
|||
if (response.type == JDBC_RESP_ERROR) { |
|||
throw new SQLException("Failed to rollback: " + new String(response.data)); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void close() throws SQLException { |
|||
if (!closed) { |
|||
try { |
|||
if (outputStream != null) { |
|||
sendMessage(JDBC_MSG_DISCONNECT, new byte[0]); |
|||
outputStream.close(); |
|||
} |
|||
if (inputStream != null) { |
|||
inputStream.close(); |
|||
} |
|||
if (socket != null && !socket.isClosed()) { |
|||
socket.close(); |
|||
} |
|||
} catch (Exception e) { |
|||
logger.warn("Error closing connection: {}", e.getMessage()); |
|||
} finally { |
|||
closed = true; |
|||
logger.debug("Connection closed"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public boolean isClosed() throws SQLException { |
|||
return closed || (socket != null && socket.isClosed()); |
|||
} |
|||
|
|||
@Override |
|||
public DatabaseMetaData getMetaData() throws SQLException { |
|||
checkClosed(); |
|||
return new SeaweedFSDatabaseMetaData(this); |
|||
} |
|||
|
|||
@Override |
|||
public void setReadOnly(boolean readOnly) throws SQLException { |
|||
checkClosed(); |
|||
// SeaweedFS is always read-only, so we ignore attempts to change this |
|||
this.readOnly = true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean isReadOnly() throws SQLException { |
|||
checkClosed(); |
|||
return readOnly; |
|||
} |
|||
|
|||
@Override |
|||
public void setCatalog(String catalog) throws SQLException { |
|||
checkClosed(); |
|||
this.catalog = catalog; |
|||
} |
|||
|
|||
@Override |
|||
public String getCatalog() throws SQLException { |
|||
checkClosed(); |
|||
return catalog != null ? catalog : connectionInfo.getDatabase(); |
|||
} |
|||
|
|||
@Override |
|||
public void setTransactionIsolation(int level) throws SQLException { |
|||
checkClosed(); |
|||
this.transactionIsolation = level; |
|||
} |
|||
|
|||
@Override |
|||
public int getTransactionIsolation() throws SQLException { |
|||
checkClosed(); |
|||
return transactionIsolation; |
|||
} |
|||
|
|||
@Override |
|||
public SQLWarning getWarnings() throws SQLException { |
|||
checkClosed(); |
|||
return null; // No warnings for now |
|||
} |
|||
|
|||
@Override |
|||
public void clearWarnings() throws SQLException { |
|||
checkClosed(); |
|||
// No-op |
|||
} |
|||
|
|||
// Methods not commonly used - basic implementations |
|||
|
|||
@Override |
|||
public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { |
|||
return createStatement(); |
|||
} |
|||
|
|||
@Override |
|||
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { |
|||
return prepareStatement(sql); |
|||
} |
|||
|
|||
@Override |
|||
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Callable statements are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public Map<String, Class<?>> getTypeMap() throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Type maps are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setTypeMap(Map<String, Class<?>> map) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Type maps are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setHoldability(int holdability) throws SQLException { |
|||
// No-op |
|||
} |
|||
|
|||
@Override |
|||
public int getHoldability() throws SQLException { |
|||
return ResultSet.CLOSE_CURSORS_AT_COMMIT; |
|||
} |
|||
|
|||
@Override |
|||
public Savepoint setSavepoint() throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Savepoints are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public Savepoint setSavepoint(String name) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Savepoints are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void rollback(Savepoint savepoint) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Savepoints are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void releaseSavepoint(Savepoint savepoint) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Savepoints are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { |
|||
return createStatement(); |
|||
} |
|||
|
|||
@Override |
|||
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { |
|||
return prepareStatement(sql); |
|||
} |
|||
|
|||
@Override |
|||
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Callable statements are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { |
|||
return prepareStatement(sql); |
|||
} |
|||
|
|||
@Override |
|||
public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { |
|||
return prepareStatement(sql); |
|||
} |
|||
|
|||
@Override |
|||
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { |
|||
return prepareStatement(sql); |
|||
} |
|||
|
|||
@Override |
|||
public Clob createClob() throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Clob creation is not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public Blob createBlob() throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Blob creation is not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public NClob createNClob() throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("NClob creation is not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public SQLXML createSQLXML() throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("SQLXML creation is not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isValid(int timeout) throws SQLException { |
|||
return !closed && socket != null && !socket.isClosed(); |
|||
} |
|||
|
|||
@Override |
|||
public void setClientInfo(String name, String value) throws SQLClientInfoException { |
|||
// No-op |
|||
} |
|||
|
|||
@Override |
|||
public void setClientInfo(Properties properties) throws SQLClientInfoException { |
|||
// No-op |
|||
} |
|||
|
|||
@Override |
|||
public String getClientInfo(String name) throws SQLException { |
|||
return null; |
|||
} |
|||
|
|||
@Override |
|||
public Properties getClientInfo() throws SQLException { |
|||
return new Properties(); |
|||
} |
|||
|
|||
@Override |
|||
public Array createArrayOf(String typeName, Object[] elements) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Array creation is not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public Struct createStruct(String typeName, Object[] attributes) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Struct creation is not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setSchema(String schema) throws SQLException { |
|||
// No-op |
|||
} |
|||
|
|||
@Override |
|||
public String getSchema() throws SQLException { |
|||
return connectionInfo.getDatabase(); |
|||
} |
|||
|
|||
@Override |
|||
public void abort(Executor executor) throws SQLException { |
|||
close(); |
|||
} |
|||
|
|||
@Override |
|||
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { |
|||
try { |
|||
if (socket != null) { |
|||
socket.setSoTimeout(milliseconds); |
|||
} |
|||
} catch (Exception e) { |
|||
throw new SQLException("Failed to set network timeout", e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public int getNetworkTimeout() throws SQLException { |
|||
try { |
|||
return socket != null ? socket.getSoTimeout() : 0; |
|||
} catch (Exception e) { |
|||
throw new SQLException("Failed to get network timeout", e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public <T> T unwrap(Class<T> iface) throws SQLException { |
|||
if (iface.isAssignableFrom(getClass())) { |
|||
return iface.cast(this); |
|||
} |
|||
throw new SQLException("Cannot unwrap to " + iface.getName()); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isWrapperFor(Class<?> iface) throws SQLException { |
|||
return iface.isAssignableFrom(getClass()); |
|||
} |
|||
|
|||
// Package-private methods for use by Statement and other classes |
|||
|
|||
void sendMessage(byte messageType, byte[] data) throws SQLException { |
|||
try { |
|||
synchronized (outputStream) { |
|||
// Write header: message type (1 byte) + data length (4 bytes) |
|||
outputStream.writeByte(messageType); |
|||
outputStream.writeInt(data.length); |
|||
|
|||
// Write data |
|||
if (data.length > 0) { |
|||
outputStream.write(data); |
|||
} |
|||
|
|||
outputStream.flush(); |
|||
} |
|||
} catch (IOException e) { |
|||
throw new SQLException("Failed to send message to server", e); |
|||
} |
|||
} |
|||
|
|||
Response readResponse() throws SQLException { |
|||
try { |
|||
synchronized (inputStream) { |
|||
// Read response type |
|||
byte responseType = inputStream.readByte(); |
|||
|
|||
// Read data length |
|||
int dataLength = inputStream.readInt(); |
|||
|
|||
// Read data |
|||
byte[] data = new byte[dataLength]; |
|||
if (dataLength > 0) { |
|||
inputStream.readFully(data); |
|||
} |
|||
|
|||
return new Response(responseType, data); |
|||
} |
|||
} catch (SocketTimeoutException e) { |
|||
throw new SQLException("Read timeout from server", e); |
|||
} catch (IOException e) { |
|||
throw new SQLException("Failed to read response from server", e); |
|||
} |
|||
} |
|||
|
|||
private void checkClosed() throws SQLException { |
|||
if (closed) { |
|||
throw new SQLException("Connection is closed"); |
|||
} |
|||
} |
|||
|
|||
SeaweedFSConnectionInfo getConnectionInfo() { |
|||
return connectionInfo; |
|||
} |
|||
|
|||
// Helper class for responses |
|||
static class Response { |
|||
final byte type; |
|||
final byte[] data; |
|||
|
|||
Response(byte type, byte[] data) { |
|||
this.type = type; |
|||
this.data = data; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,71 @@ |
|||
package com.seaweedfs.jdbc; |
|||
|
|||
import java.util.Properties; |
|||
|
|||
/** |
|||
* Connection information holder for SeaweedFS JDBC connections |
|||
*/ |
|||
public class SeaweedFSConnectionInfo { |
|||
|
|||
private final String host; |
|||
private final int port; |
|||
private final String database; |
|||
private final String user; |
|||
private final String password; |
|||
private final int connectTimeout; |
|||
private final int socketTimeout; |
|||
private final Properties properties; |
|||
|
|||
public SeaweedFSConnectionInfo(Properties props) { |
|||
this.properties = new Properties(props); |
|||
this.host = props.getProperty(SeaweedFSDriver.PROP_HOST, "localhost"); |
|||
this.port = Integer.parseInt(props.getProperty(SeaweedFSDriver.PROP_PORT, "8089")); |
|||
this.database = props.getProperty(SeaweedFSDriver.PROP_DATABASE, "default"); |
|||
this.user = props.getProperty(SeaweedFSDriver.PROP_USER, ""); |
|||
this.password = props.getProperty(SeaweedFSDriver.PROP_PASSWORD, ""); |
|||
this.connectTimeout = Integer.parseInt(props.getProperty(SeaweedFSDriver.PROP_CONNECT_TIMEOUT, "30000")); |
|||
this.socketTimeout = Integer.parseInt(props.getProperty(SeaweedFSDriver.PROP_SOCKET_TIMEOUT, "0")); |
|||
} |
|||
|
|||
public String getHost() { |
|||
return host; |
|||
} |
|||
|
|||
public int getPort() { |
|||
return port; |
|||
} |
|||
|
|||
public String getDatabase() { |
|||
return database; |
|||
} |
|||
|
|||
public String getUser() { |
|||
return user; |
|||
} |
|||
|
|||
public String getPassword() { |
|||
return password; |
|||
} |
|||
|
|||
public int getConnectTimeout() { |
|||
return connectTimeout; |
|||
} |
|||
|
|||
public int getSocketTimeout() { |
|||
return socketTimeout; |
|||
} |
|||
|
|||
public Properties getProperties() { |
|||
return new Properties(properties); |
|||
} |
|||
|
|||
public String getConnectionString() { |
|||
return String.format("jdbc:seaweedfs://%s:%d/%s", host, port, database); |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return String.format("SeaweedFSConnectionInfo{host='%s', port=%d, database='%s', user='%s', connectTimeout=%d, socketTimeout=%d}", |
|||
host, port, database, user, connectTimeout, socketTimeout); |
|||
} |
|||
} |
@ -0,0 +1,972 @@ |
|||
package com.seaweedfs.jdbc; |
|||
|
|||
import java.sql.*; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* DatabaseMetaData implementation for SeaweedFS JDBC |
|||
*/ |
|||
public class SeaweedFSDatabaseMetaData implements DatabaseMetaData { |
|||
|
|||
private final SeaweedFSConnection connection; |
|||
|
|||
public SeaweedFSDatabaseMetaData(SeaweedFSConnection connection) { |
|||
this.connection = connection; |
|||
} |
|||
|
|||
@Override |
|||
public boolean allProceduresAreCallable() throws SQLException { |
|||
return false; // No stored procedures |
|||
} |
|||
|
|||
@Override |
|||
public boolean allTablesAreSelectable() throws SQLException { |
|||
return true; // All tables are selectable |
|||
} |
|||
|
|||
@Override |
|||
public String getURL() throws SQLException { |
|||
return connection.getConnectionInfo().getConnectionString(); |
|||
} |
|||
|
|||
@Override |
|||
public String getUserName() throws SQLException { |
|||
return connection.getConnectionInfo().getUser(); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isReadOnly() throws SQLException { |
|||
return true; // SeaweedFS is read-only |
|||
} |
|||
|
|||
@Override |
|||
public boolean nullsAreSortedHigh() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean nullsAreSortedLow() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean nullsAreSortedAtStart() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean nullsAreSortedAtEnd() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public String getDatabaseProductName() throws SQLException { |
|||
return "SeaweedFS"; |
|||
} |
|||
|
|||
@Override |
|||
public String getDatabaseProductVersion() throws SQLException { |
|||
return "1.0.0"; // This could be retrieved from the server |
|||
} |
|||
|
|||
@Override |
|||
public String getDriverName() throws SQLException { |
|||
return SeaweedFSDriver.DRIVER_NAME; |
|||
} |
|||
|
|||
@Override |
|||
public String getDriverVersion() throws SQLException { |
|||
return SeaweedFSDriver.DRIVER_VERSION; |
|||
} |
|||
|
|||
@Override |
|||
public int getDriverMajorVersion() { |
|||
return SeaweedFSDriver.DRIVER_MAJOR_VERSION; |
|||
} |
|||
|
|||
@Override |
|||
public int getDriverMinorVersion() { |
|||
return SeaweedFSDriver.DRIVER_MINOR_VERSION; |
|||
} |
|||
|
|||
@Override |
|||
public boolean usesLocalFiles() throws SQLException { |
|||
return false; // SeaweedFS uses distributed storage |
|||
} |
|||
|
|||
@Override |
|||
public boolean usesLocalFilePerTable() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsMixedCaseIdentifiers() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean storesUpperCaseIdentifiers() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean storesLowerCaseIdentifiers() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean storesMixedCaseIdentifiers() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean storesUpperCaseQuotedIdentifiers() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean storesLowerCaseQuotedIdentifiers() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean storesMixedCaseQuotedIdentifiers() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public String getIdentifierQuoteString() throws SQLException { |
|||
return "`"; |
|||
} |
|||
|
|||
@Override |
|||
public String getSQLKeywords() throws SQLException { |
|||
return ""; // No additional keywords beyond SQL standard |
|||
} |
|||
|
|||
@Override |
|||
public String getNumericFunctions() throws SQLException { |
|||
return "COUNT,SUM,AVG,MIN,MAX"; |
|||
} |
|||
|
|||
@Override |
|||
public String getStringFunctions() throws SQLException { |
|||
return ""; |
|||
} |
|||
|
|||
@Override |
|||
public String getSystemFunctions() throws SQLException { |
|||
return ""; |
|||
} |
|||
|
|||
@Override |
|||
public String getTimeDateFunctions() throws SQLException { |
|||
return ""; |
|||
} |
|||
|
|||
@Override |
|||
public String getSearchStringEscape() throws SQLException { |
|||
return "\\"; |
|||
} |
|||
|
|||
@Override |
|||
public String getExtraNameCharacters() throws SQLException { |
|||
return ""; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsAlterTableWithAddColumn() throws SQLException { |
|||
return false; // No DDL support |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsAlterTableWithDropColumn() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsColumnAliasing() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean nullPlusNonNullIsNull() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsConvert() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsConvert(int fromType, int toType) throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsTableCorrelationNames() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsDifferentTableCorrelationNames() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsExpressionsInOrderBy() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsOrderByUnrelated() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsGroupBy() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsGroupByUnrelated() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsGroupByBeyondSelect() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsLikeEscapeClause() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsMultipleResultSets() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsMultipleTransactions() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsNonNullableColumns() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsMinimumSQLGrammar() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsCoreSQLGrammar() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsExtendedSQLGrammar() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsANSI92EntryLevelSQL() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsANSI92IntermediateSQL() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsANSI92FullSQL() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsIntegrityEnhancementFacility() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsOuterJoins() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsFullOuterJoins() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsLimitedOuterJoins() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public String getSchemaTerm() throws SQLException { |
|||
return "schema"; |
|||
} |
|||
|
|||
@Override |
|||
public String getProcedureTerm() throws SQLException { |
|||
return "procedure"; |
|||
} |
|||
|
|||
@Override |
|||
public String getCatalogTerm() throws SQLException { |
|||
return "catalog"; |
|||
} |
|||
|
|||
@Override |
|||
public boolean isCatalogAtStart() throws SQLException { |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public String getCatalogSeparator() throws SQLException { |
|||
return "."; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsSchemasInDataManipulation() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsSchemasInProcedureCalls() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsSchemasInTableDefinitions() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsSchemasInIndexDefinitions() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsCatalogsInDataManipulation() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsCatalogsInProcedureCalls() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsCatalogsInTableDefinitions() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsCatalogsInIndexDefinitions() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsPositionedDelete() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsPositionedUpdate() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsSelectForUpdate() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsStoredProcedures() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsSubqueriesInComparisons() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsSubqueriesInExists() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsSubqueriesInIns() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsSubqueriesInQuantifieds() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsCorrelatedSubqueries() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsUnion() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsUnionAll() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsOpenCursorsAcrossCommit() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsOpenCursorsAcrossRollback() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsOpenStatementsAcrossCommit() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsOpenStatementsAcrossRollback() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxBinaryLiteralLength() throws SQLException { |
|||
return 0; // No limit |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxCharLiteralLength() throws SQLException { |
|||
return 0; // No limit |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxColumnNameLength() throws SQLException { |
|||
return 255; |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxColumnsInGroupBy() throws SQLException { |
|||
return 0; // No limit |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxColumnsInIndex() throws SQLException { |
|||
return 0; // No indexes |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxColumnsInOrderBy() throws SQLException { |
|||
return 0; // No limit |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxColumnsInSelect() throws SQLException { |
|||
return 0; // No limit |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxColumnsInTable() throws SQLException { |
|||
return 0; // No limit |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxConnections() throws SQLException { |
|||
return 0; // No limit |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxCursorNameLength() throws SQLException { |
|||
return 0; // No cursors |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxIndexLength() throws SQLException { |
|||
return 0; // No indexes |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxSchemaNameLength() throws SQLException { |
|||
return 255; |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxProcedureNameLength() throws SQLException { |
|||
return 0; // No procedures |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxCatalogNameLength() throws SQLException { |
|||
return 255; |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxRowSize() throws SQLException { |
|||
return 0; // No limit |
|||
} |
|||
|
|||
@Override |
|||
public boolean doesMaxRowSizeIncludeBlobs() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxStatementLength() throws SQLException { |
|||
return 0; // No limit |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxStatements() throws SQLException { |
|||
return 0; // No limit |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxTableNameLength() throws SQLException { |
|||
return 255; |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxTablesInSelect() throws SQLException { |
|||
return 1; // Only single table selects supported |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxUserNameLength() throws SQLException { |
|||
return 255; |
|||
} |
|||
|
|||
@Override |
|||
public int getDefaultTransactionIsolation() throws SQLException { |
|||
return Connection.TRANSACTION_NONE; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsTransactions() throws SQLException { |
|||
return false; // No transactions |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsTransactionIsolationLevel(int level) throws SQLException { |
|||
return level == Connection.TRANSACTION_NONE; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsDataManipulationTransactionsOnly() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean dataDefinitionCausesTransactionCommit() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean dataDefinitionIgnoredInTransactions() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { |
|||
// Return empty result set - no procedures |
|||
return createEmptyResultSet(new String[]{"PROCEDURE_CAT", "PROCEDURE_SCHEM", "PROCEDURE_NAME", "reserved1", "reserved2", "reserved3", "REMARKS", "PROCEDURE_TYPE", "SPECIFIC_NAME"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException { |
|||
// Return empty result set - no procedures |
|||
return createEmptyResultSet(new String[]{"PROCEDURE_CAT", "PROCEDURE_SCHEM", "PROCEDURE_NAME", "COLUMN_NAME", "COLUMN_TYPE", "DATA_TYPE", "TYPE_NAME", "PRECISION", "LENGTH", "SCALE", "RADIX", "NULLABLE", "REMARKS", "COLUMN_DEF", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "CHAR_OCTET_LENGTH", "ORDINAL_POSITION", "IS_NULLABLE", "SPECIFIC_NAME"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) throws SQLException { |
|||
// For now, return empty result set |
|||
// In a full implementation, this would query the schema catalog |
|||
return createEmptyResultSet(new String[]{"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "TABLE_TYPE", "REMARKS", "TYPE_CAT", "TYPE_SCHEM", "TYPE_NAME", "SELF_REFERENCING_COL_NAME", "REF_GENERATION"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getSchemas() throws SQLException { |
|||
return getSchemas(null, null); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getCatalogs() throws SQLException { |
|||
// Return default catalog |
|||
List<List<String>> rows = new ArrayList<>(); |
|||
rows.add(List.of("default")); |
|||
return createResultSet(new String[]{"TABLE_CAT"}, rows); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getTableTypes() throws SQLException { |
|||
List<List<String>> rows = new ArrayList<>(); |
|||
rows.add(List.of("TABLE")); |
|||
return createResultSet(new String[]{"TABLE_TYPE"}, rows); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { |
|||
// Return empty result set for now |
|||
return createEmptyResultSet(new String[]{"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "DATA_TYPE", "TYPE_NAME", "COLUMN_SIZE", "BUFFER_LENGTH", "DECIMAL_DIGITS", "NUM_PREC_RADIX", "NULLABLE", "REMARKS", "COLUMN_DEF", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "CHAR_OCTET_LENGTH", "ORDINAL_POSITION", "IS_NULLABLE", "SCOPE_CATALOG", "SCOPE_SCHEMA", "SCOPE_TABLE", "SOURCE_DATA_TYPE", "IS_AUTOINCREMENT", "IS_GENERATEDCOLUMN"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "GRANTOR", "GRANTEE", "PRIVILEGE", "IS_GRANTABLE"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "GRANTOR", "GRANTEE", "PRIVILEGE", "IS_GRANTABLE"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"SCOPE", "COLUMN_NAME", "DATA_TYPE", "TYPE_NAME", "COLUMN_SIZE", "BUFFER_LENGTH", "DECIMAL_DIGITS", "PSEUDO_COLUMN"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"SCOPE", "COLUMN_NAME", "DATA_TYPE", "TYPE_NAME", "COLUMN_SIZE", "BUFFER_LENGTH", "DECIMAL_DIGITS", "PSEUDO_COLUMN"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"PKTABLE_CAT", "PKTABLE_SCHEM", "PKTABLE_NAME", "PKCOLUMN_NAME", "FKTABLE_CAT", "FKTABLE_SCHEM", "FKTABLE_NAME", "FKCOLUMN_NAME", "KEY_SEQ", "UPDATE_RULE", "DELETE_RULE", "FK_NAME", "PK_NAME", "DEFERRABILITY"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"PKTABLE_CAT", "PKTABLE_SCHEM", "PKTABLE_NAME", "PKCOLUMN_NAME", "FKTABLE_CAT", "FKTABLE_SCHEM", "FKTABLE_NAME", "FKCOLUMN_NAME", "KEY_SEQ", "UPDATE_RULE", "DELETE_RULE", "FK_NAME", "PK_NAME", "DEFERRABILITY"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"PKTABLE_CAT", "PKTABLE_SCHEM", "PKTABLE_NAME", "PKCOLUMN_NAME", "FKTABLE_CAT", "FKTABLE_SCHEM", "FKTABLE_NAME", "FKCOLUMN_NAME", "KEY_SEQ", "UPDATE_RULE", "DELETE_RULE", "FK_NAME", "PK_NAME", "DEFERRABILITY"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getTypeInfo() throws SQLException { |
|||
List<List<String>> rows = new ArrayList<>(); |
|||
// Add basic SQL types |
|||
rows.add(List.of("VARCHAR", String.valueOf(Types.VARCHAR), "65535", "'", "'", "length", "1", "3", "1", "0", "0", "0", "VARCHAR", "0", "0", String.valueOf(Types.VARCHAR), "0", "10")); |
|||
rows.add(List.of("BIGINT", String.valueOf(Types.BIGINT), "19", null, null, null, "1", "2", "0", "0", "0", "1", "BIGINT", "0", "0", String.valueOf(Types.BIGINT), "0", "10")); |
|||
rows.add(List.of("BOOLEAN", String.valueOf(Types.BOOLEAN), "1", null, null, null, "1", "2", "0", "0", "0", "1", "BOOLEAN", "0", "0", String.valueOf(Types.BOOLEAN), "0", "10")); |
|||
rows.add(List.of("TIMESTAMP", String.valueOf(Types.TIMESTAMP), "23", "'", "'", null, "1", "3", "0", "0", "0", "0", "TIMESTAMP", "0", "6", String.valueOf(Types.TIMESTAMP), "0", "10")); |
|||
|
|||
return createResultSet(new String[]{"TYPE_NAME", "DATA_TYPE", "PRECISION", "LITERAL_PREFIX", "LITERAL_SUFFIX", "CREATE_PARAMS", "NULLABLE", "CASE_SENSITIVE", "SEARCHABLE", "UNSIGNED_ATTRIBUTE", "FIXED_PREC_SCALE", "AUTO_INCREMENT", "LOCAL_TYPE_NAME", "MINIMUM_SCALE", "MAXIMUM_SCALE", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "NUM_PREC_RADIX"}, rows); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "NON_UNIQUE", "INDEX_QUALIFIER", "INDEX_NAME", "TYPE", "ORDINAL_POSITION", "COLUMN_NAME", "ASC_OR_DESC", "CARDINALITY", "PAGES", "FILTER_CONDITION"}); |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsResultSetType(int type) throws SQLException { |
|||
return type == ResultSet.TYPE_FORWARD_ONLY; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { |
|||
return type == ResultSet.TYPE_FORWARD_ONLY && concurrency == ResultSet.CONCUR_READ_ONLY; |
|||
} |
|||
|
|||
@Override |
|||
public boolean ownUpdatesAreVisible(int type) throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean ownDeletesAreVisible(int type) throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean ownInsertsAreVisible(int type) throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean othersUpdatesAreVisible(int type) throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean othersDeletesAreVisible(int type) throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean othersInsertsAreVisible(int type) throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean updatesAreDetected(int type) throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean deletesAreDetected(int type) throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean insertsAreDetected(int type) throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsBatchUpdates() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"TYPE_CAT", "TYPE_SCHEM", "TYPE_NAME", "CLASS_NAME", "DATA_TYPE", "REMARKS", "BASE_TYPE"}); |
|||
} |
|||
|
|||
@Override |
|||
public Connection getConnection() throws SQLException { |
|||
return connection; |
|||
} |
|||
|
|||
// JDBC 3.0 methods |
|||
@Override |
|||
public boolean supportsSavepoints() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsNamedParameters() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsMultipleOpenResults() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsGetGeneratedKeys() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"TYPE_CAT", "TYPE_SCHEM", "TYPE_NAME", "SUPERTYPE_CAT", "SUPERTYPE_SCHEM", "SUPERTYPE_NAME"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "SUPERTABLE_NAME"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"TYPE_CAT", "TYPE_SCHEM", "TYPE_NAME", "ATTR_NAME", "DATA_TYPE", "ATTR_TYPE_NAME", "ATTR_SIZE", "DECIMAL_DIGITS", "NUM_PREC_RADIX", "NULLABLE", "REMARKS", "ATTR_DEF", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "CHAR_OCTET_LENGTH", "ORDINAL_POSITION", "IS_NULLABLE", "SCOPE_CATALOG", "SCOPE_SCHEMA", "SCOPE_TABLE", "SOURCE_DATA_TYPE"}); |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsResultSetHoldability(int holdability) throws SQLException { |
|||
return holdability == ResultSet.CLOSE_CURSORS_AT_COMMIT; |
|||
} |
|||
|
|||
@Override |
|||
public int getResultSetHoldability() throws SQLException { |
|||
return ResultSet.CLOSE_CURSORS_AT_COMMIT; |
|||
} |
|||
|
|||
@Override |
|||
public int getDatabaseMajorVersion() throws SQLException { |
|||
return 1; |
|||
} |
|||
|
|||
@Override |
|||
public int getDatabaseMinorVersion() throws SQLException { |
|||
return 0; |
|||
} |
|||
|
|||
@Override |
|||
public int getJDBCMajorVersion() throws SQLException { |
|||
return 4; |
|||
} |
|||
|
|||
@Override |
|||
public int getJDBCMinorVersion() throws SQLException { |
|||
return 0; |
|||
} |
|||
|
|||
@Override |
|||
public int getSQLStateType() throws SQLException { |
|||
return DatabaseMetaData.sqlStateSQL; |
|||
} |
|||
|
|||
@Override |
|||
public boolean locatorsUpdateCopy() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsStatementPooling() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
// JDBC 4.0 methods |
|||
@Override |
|||
public RowIdLifetime getRowIdLifetime() throws SQLException { |
|||
return RowIdLifetime.ROWID_UNSUPPORTED; |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { |
|||
List<List<String>> rows = new ArrayList<>(); |
|||
rows.add(List.of(connection.getConnectionInfo().getDatabase(), "default")); |
|||
return createResultSet(new String[]{"TABLE_SCHEM", "TABLE_CATALOG"}, rows); |
|||
} |
|||
|
|||
@Override |
|||
public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean autoCommitFailureClosesAllResultSets() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getClientInfoProperties() throws SQLException { |
|||
return createEmptyResultSet(new String[]{"NAME", "MAX_LEN", "DEFAULT_VALUE", "DESCRIPTION"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"FUNCTION_CAT", "FUNCTION_SCHEM", "FUNCTION_NAME", "REMARKS", "FUNCTION_TYPE", "SPECIFIC_NAME"}); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"FUNCTION_CAT", "FUNCTION_SCHEM", "FUNCTION_NAME", "COLUMN_NAME", "COLUMN_TYPE", "DATA_TYPE", "TYPE_NAME", "PRECISION", "LENGTH", "SCALE", "RADIX", "NULLABLE", "REMARKS", "CHAR_OCTET_LENGTH", "ORDINAL_POSITION", "IS_NULLABLE", "SPECIFIC_NAME"}); |
|||
} |
|||
|
|||
// JDBC 4.1 methods |
|||
@Override |
|||
public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { |
|||
return createEmptyResultSet(new String[]{"TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "DATA_TYPE", "COLUMN_SIZE", "DECIMAL_DIGITS", "NUM_PREC_RADIX", "COLUMN_USAGE", "REMARKS", "CHAR_OCTET_LENGTH", "IS_NULLABLE"}); |
|||
} |
|||
|
|||
@Override |
|||
public boolean generatedKeyAlwaysReturned() throws SQLException { |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public <T> T unwrap(Class<T> iface) throws SQLException { |
|||
if (iface.isAssignableFrom(getClass())) { |
|||
return iface.cast(this); |
|||
} |
|||
throw new SQLException("Cannot unwrap to " + iface.getName()); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isWrapperFor(Class<?> iface) throws SQLException { |
|||
return iface.isAssignableFrom(getClass()); |
|||
} |
|||
|
|||
// Helper methods to create result sets |
|||
private ResultSet createEmptyResultSet(String[] columnNames) throws SQLException { |
|||
return createResultSet(columnNames, new ArrayList<>()); |
|||
} |
|||
|
|||
private ResultSet createResultSet(String[] columnNames, List<List<String>> rows) throws SQLException { |
|||
// Convert to the format expected by SeaweedFSResultSet |
|||
byte[] data = serializeResultSetData(columnNames, rows); |
|||
return new SeaweedFSResultSet(null, data); |
|||
} |
|||
|
|||
private byte[] serializeResultSetData(String[] columnNames, List<List<String>> rows) { |
|||
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); |
|||
java.io.DataOutputStream dos = new java.io.DataOutputStream(baos); |
|||
|
|||
try { |
|||
// Column count |
|||
dos.writeInt(columnNames.length); |
|||
|
|||
// Column names |
|||
for (String name : columnNames) { |
|||
byte[] nameBytes = name.getBytes(); |
|||
dos.writeInt(nameBytes.length); |
|||
dos.write(nameBytes); |
|||
} |
|||
|
|||
// Row count |
|||
dos.writeInt(rows.size()); |
|||
|
|||
// Rows |
|||
for (List<String> row : rows) { |
|||
for (String value : row) { |
|||
if (value != null) { |
|||
byte[] valueBytes = value.getBytes(); |
|||
dos.writeInt(valueBytes.length); |
|||
dos.write(valueBytes); |
|||
} else { |
|||
dos.writeInt(0); // null value |
|||
} |
|||
} |
|||
} |
|||
|
|||
dos.flush(); |
|||
return baos.toByteArray(); |
|||
|
|||
} catch (Exception e) { |
|||
throw new RuntimeException("Failed to serialize result set data", e); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,207 @@ |
|||
package com.seaweedfs.jdbc; |
|||
|
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
|
|||
import java.sql.*; |
|||
import java.util.Properties; |
|||
|
|||
/** |
|||
* SeaweedFS JDBC Driver |
|||
* |
|||
* Provides JDBC connectivity to SeaweedFS SQL engine for querying MQ topics. |
|||
* |
|||
* JDBC URL format: jdbc:seaweedfs://host:port/database |
|||
* |
|||
* Example usage: |
|||
* <pre> |
|||
* Class.forName("com.seaweedfs.jdbc.SeaweedFSDriver"); |
|||
* Connection conn = DriverManager.getConnection("jdbc:seaweedfs://localhost:8089/default"); |
|||
* Statement stmt = conn.createStatement(); |
|||
* ResultSet rs = stmt.executeQuery("SELECT * FROM my_topic LIMIT 10"); |
|||
* </pre> |
|||
*/ |
|||
public class SeaweedFSDriver implements Driver { |
|||
|
|||
private static final Logger logger = LoggerFactory.getLogger(SeaweedFSDriver.class); |
|||
|
|||
// Driver information |
|||
public static final String DRIVER_NAME = "SeaweedFS JDBC Driver"; |
|||
public static final String DRIVER_VERSION = "1.0.0"; |
|||
public static final int DRIVER_MAJOR_VERSION = 1; |
|||
public static final int DRIVER_MINOR_VERSION = 0; |
|||
|
|||
// URL prefix for SeaweedFS JDBC connections |
|||
public static final String URL_PREFIX = "jdbc:seaweedfs://"; |
|||
|
|||
// Default connection properties |
|||
public static final String PROP_HOST = "host"; |
|||
public static final String PROP_PORT = "port"; |
|||
public static final String PROP_DATABASE = "database"; |
|||
public static final String PROP_USER = "user"; |
|||
public static final String PROP_PASSWORD = "password"; |
|||
public static final String PROP_CONNECT_TIMEOUT = "connectTimeout"; |
|||
public static final String PROP_SOCKET_TIMEOUT = "socketTimeout"; |
|||
|
|||
static { |
|||
try { |
|||
// Register the driver with the DriverManager |
|||
DriverManager.registerDriver(new SeaweedFSDriver()); |
|||
logger.info("SeaweedFS JDBC Driver {} registered successfully", DRIVER_VERSION); |
|||
} catch (SQLException e) { |
|||
logger.error("Failed to register SeaweedFS JDBC Driver", e); |
|||
throw new RuntimeException("Failed to register SeaweedFS JDBC Driver", e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Connection connect(String url, Properties info) throws SQLException { |
|||
if (!acceptsURL(url)) { |
|||
return null; // Not our URL, let another driver handle it |
|||
} |
|||
|
|||
logger.debug("Attempting to connect to: {}", url); |
|||
|
|||
try { |
|||
// Parse the URL to extract connection parameters |
|||
SeaweedFSConnectionInfo connectionInfo = parseURL(url, info); |
|||
|
|||
// Create and return the connection |
|||
return new SeaweedFSConnection(connectionInfo); |
|||
|
|||
} catch (Exception e) { |
|||
logger.error("Failed to connect to SeaweedFS: {}", e.getMessage(), e); |
|||
throw new SQLException("Failed to connect to SeaweedFS: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public boolean acceptsURL(String url) throws SQLException { |
|||
return url != null && url.startsWith(URL_PREFIX); |
|||
} |
|||
|
|||
@Override |
|||
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { |
|||
return new DriverPropertyInfo[] { |
|||
createPropertyInfo(PROP_HOST, "localhost", "SeaweedFS JDBC server hostname", null, false), |
|||
createPropertyInfo(PROP_PORT, "8089", "SeaweedFS JDBC server port", null, false), |
|||
createPropertyInfo(PROP_DATABASE, "default", "Database/namespace name", null, false), |
|||
createPropertyInfo(PROP_USER, "", "Username (optional)", null, false), |
|||
createPropertyInfo(PROP_PASSWORD, "", "Password (optional)", null, false), |
|||
createPropertyInfo(PROP_CONNECT_TIMEOUT, "30000", "Connection timeout in milliseconds", null, false), |
|||
createPropertyInfo(PROP_SOCKET_TIMEOUT, "0", "Socket timeout in milliseconds (0 = infinite)", null, false) |
|||
}; |
|||
} |
|||
|
|||
private DriverPropertyInfo createPropertyInfo(String name, String defaultValue, String description, String[] choices, boolean required) { |
|||
DriverPropertyInfo info = new DriverPropertyInfo(name, defaultValue); |
|||
info.description = description; |
|||
info.choices = choices; |
|||
info.required = required; |
|||
return info; |
|||
} |
|||
|
|||
@Override |
|||
public int getMajorVersion() { |
|||
return DRIVER_MAJOR_VERSION; |
|||
} |
|||
|
|||
@Override |
|||
public int getMinorVersion() { |
|||
return DRIVER_MINOR_VERSION; |
|||
} |
|||
|
|||
@Override |
|||
public boolean jdbcCompliant() { |
|||
// We implement a subset of JDBC, so we're not fully compliant |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException { |
|||
throw new SQLFeatureNotSupportedException("getParentLogger is not supported"); |
|||
} |
|||
|
|||
/** |
|||
* Parse JDBC URL and extract connection information |
|||
* |
|||
* Expected format: jdbc:seaweedfs://host:port/database[?property=value&...] |
|||
*/ |
|||
private SeaweedFSConnectionInfo parseURL(String url, Properties info) throws SQLException { |
|||
if (!acceptsURL(url)) { |
|||
throw new SQLException("Invalid SeaweedFS JDBC URL: " + url); |
|||
} |
|||
|
|||
try { |
|||
// Remove the jdbc:seaweedfs:// prefix |
|||
String remaining = url.substring(URL_PREFIX.length()); |
|||
|
|||
// Split into host:port/database and query parameters |
|||
String[] parts = remaining.split("\\?", 2); |
|||
String hostPortDb = parts[0]; |
|||
String queryParams = parts.length > 1 ? parts[1] : ""; |
|||
|
|||
// Parse host, port, and database |
|||
String host = "localhost"; |
|||
int port = 8089; |
|||
String database = "default"; |
|||
|
|||
if (hostPortDb.contains("/")) { |
|||
String[] hostPortDbParts = hostPortDb.split("/", 2); |
|||
String hostPort = hostPortDbParts[0]; |
|||
database = hostPortDbParts[1]; |
|||
|
|||
if (hostPort.contains(":")) { |
|||
String[] hostPortParts = hostPort.split(":", 2); |
|||
host = hostPortParts[0]; |
|||
port = Integer.parseInt(hostPortParts[1]); |
|||
} else { |
|||
host = hostPort; |
|||
} |
|||
} else if (hostPortDb.contains(":")) { |
|||
String[] hostPortParts = hostPortDb.split(":", 2); |
|||
host = hostPortParts[0]; |
|||
port = Integer.parseInt(hostPortParts[1]); |
|||
} else if (!hostPortDb.isEmpty()) { |
|||
host = hostPortDb; |
|||
} |
|||
|
|||
// Create properties with defaults |
|||
Properties connectionProps = new Properties(); |
|||
connectionProps.setProperty(PROP_HOST, host); |
|||
connectionProps.setProperty(PROP_PORT, String.valueOf(port)); |
|||
connectionProps.setProperty(PROP_DATABASE, database); |
|||
connectionProps.setProperty(PROP_USER, ""); |
|||
connectionProps.setProperty(PROP_PASSWORD, ""); |
|||
connectionProps.setProperty(PROP_CONNECT_TIMEOUT, "30000"); |
|||
connectionProps.setProperty(PROP_SOCKET_TIMEOUT, "0"); |
|||
|
|||
// Override with provided properties |
|||
if (info != null) { |
|||
connectionProps.putAll(info); |
|||
} |
|||
|
|||
// Parse query parameters |
|||
if (!queryParams.isEmpty()) { |
|||
for (String param : queryParams.split("&")) { |
|||
String[] keyValue = param.split("=", 2); |
|||
if (keyValue.length == 2) { |
|||
connectionProps.setProperty(keyValue[0], keyValue[1]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new SeaweedFSConnectionInfo(connectionProps); |
|||
|
|||
} catch (Exception e) { |
|||
throw new SQLException("Failed to parse SeaweedFS JDBC URL: " + url, e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get driver information string |
|||
*/ |
|||
public static String getDriverInfo() { |
|||
return String.format("%s v%s", DRIVER_NAME, DRIVER_VERSION); |
|||
} |
|||
} |
@ -0,0 +1,352 @@ |
|||
package com.seaweedfs.jdbc; |
|||
|
|||
import java.io.InputStream; |
|||
import java.io.Reader; |
|||
import java.math.BigDecimal; |
|||
import java.net.URL; |
|||
import java.sql.*; |
|||
import java.util.Calendar; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* PreparedStatement implementation for SeaweedFS JDBC |
|||
*/ |
|||
public class SeaweedFSPreparedStatement extends SeaweedFSStatement implements PreparedStatement { |
|||
|
|||
private final String originalSql; |
|||
private final Map<Integer, Object> parameters = new HashMap<>(); |
|||
|
|||
public SeaweedFSPreparedStatement(SeaweedFSConnection connection, String sql) { |
|||
super(connection); |
|||
this.originalSql = sql; |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet executeQuery() throws SQLException { |
|||
return executeQuery(buildSqlWithParameters()); |
|||
} |
|||
|
|||
@Override |
|||
public int executeUpdate() throws SQLException { |
|||
return executeUpdate(buildSqlWithParameters()); |
|||
} |
|||
|
|||
@Override |
|||
public void setNull(int parameterIndex, int sqlType) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, null); |
|||
} |
|||
|
|||
@Override |
|||
public void setBoolean(int parameterIndex, boolean x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setByte(int parameterIndex, byte x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setShort(int parameterIndex, short x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setInt(int parameterIndex, int x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setLong(int parameterIndex, long x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setFloat(int parameterIndex, float x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setDouble(int parameterIndex, double x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setString(int parameterIndex, String x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setBytes(int parameterIndex, byte[] x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setDate(int parameterIndex, Date x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setTime(int parameterIndex, Time x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("ASCII streams are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Unicode streams are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Binary streams are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void clearParameters() throws SQLException { |
|||
checkClosed(); |
|||
parameters.clear(); |
|||
} |
|||
|
|||
@Override |
|||
public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { |
|||
setObject(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setObject(int parameterIndex, Object x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public boolean execute() throws SQLException { |
|||
return execute(buildSqlWithParameters()); |
|||
} |
|||
|
|||
@Override |
|||
public void addBatch() throws SQLException { |
|||
checkClosed(); |
|||
addBatch(buildSqlWithParameters()); |
|||
} |
|||
|
|||
@Override |
|||
public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Character streams are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setRef(int parameterIndex, Ref x) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Ref objects are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setBlob(int parameterIndex, Blob x) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Blob objects are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setClob(int parameterIndex, Clob x) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Clob objects are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setArray(int parameterIndex, Array x) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Array objects are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSetMetaData getMetaData() throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Prepared statement metadata is not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { |
|||
setDate(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { |
|||
setTime(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { |
|||
setTimestamp(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { |
|||
setNull(parameterIndex, sqlType); |
|||
} |
|||
|
|||
@Override |
|||
public void setURL(int parameterIndex, URL x) throws SQLException { |
|||
checkClosed(); |
|||
parameters.put(parameterIndex, x.toString()); |
|||
} |
|||
|
|||
@Override |
|||
public ParameterMetaData getParameterMetaData() throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Parameter metadata is not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setRowId(int parameterIndex, RowId x) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("RowId objects are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setNString(int parameterIndex, String value) throws SQLException { |
|||
setString(parameterIndex, value); |
|||
} |
|||
|
|||
@Override |
|||
public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("NCharacter streams are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setNClob(int parameterIndex, NClob value) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("NClob objects are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Clob objects are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Blob objects are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("NClob objects are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("SQLXML objects are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { |
|||
setObject(parameterIndex, x); |
|||
} |
|||
|
|||
@Override |
|||
public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("ASCII streams are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Binary streams are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Character streams are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("ASCII streams are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Binary streams are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Character streams are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("NCharacter streams are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setClob(int parameterIndex, Reader reader) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Clob objects are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Blob objects are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public void setNClob(int parameterIndex, Reader reader) throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("NClob objects are not supported"); |
|||
} |
|||
|
|||
/** |
|||
* Build the final SQL string by replacing parameter placeholders with actual values |
|||
*/ |
|||
private String buildSqlWithParameters() throws SQLException { |
|||
String sql = originalSql; |
|||
|
|||
// Simple parameter substitution (not SQL-injection safe, but good enough for demo) |
|||
// In a production implementation, you would use proper parameter binding |
|||
for (Map.Entry<Integer, Object> entry : parameters.entrySet()) { |
|||
String placeholder = "\\?"; // Find first ? placeholder |
|||
String replacement; |
|||
|
|||
Object value = entry.getValue(); |
|||
if (value == null) { |
|||
replacement = "NULL"; |
|||
} else if (value instanceof String) { |
|||
// Escape single quotes and wrap in quotes |
|||
replacement = "'" + value.toString().replace("'", "''") + "'"; |
|||
} else if (value instanceof Number || value instanceof Boolean) { |
|||
replacement = value.toString(); |
|||
} else if (value instanceof Date) { |
|||
replacement = "'" + value.toString() + "'"; |
|||
} else if (value instanceof Timestamp) { |
|||
replacement = "'" + value.toString() + "'"; |
|||
} else { |
|||
replacement = "'" + value.toString().replace("'", "''") + "'"; |
|||
} |
|||
|
|||
// Replace the first occurrence of ? |
|||
sql = sql.replaceFirst(placeholder, replacement); |
|||
} |
|||
|
|||
return sql; |
|||
} |
|||
} |
1245
jdbc-driver/src/main/java/com/seaweedfs/jdbc/SeaweedFSResultSet.java
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,202 @@ |
|||
package com.seaweedfs.jdbc; |
|||
|
|||
import java.sql.ResultSetMetaData; |
|||
import java.sql.SQLException; |
|||
import java.sql.Types; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* ResultSetMetaData implementation for SeaweedFS JDBC |
|||
*/ |
|||
public class SeaweedFSResultSetMetaData implements ResultSetMetaData { |
|||
|
|||
private final List<String> columnNames; |
|||
|
|||
public SeaweedFSResultSetMetaData(List<String> columnNames) { |
|||
this.columnNames = columnNames; |
|||
} |
|||
|
|||
@Override |
|||
public int getColumnCount() throws SQLException { |
|||
return columnNames.size(); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isAutoIncrement(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return false; // SeaweedFS doesn't have auto-increment columns |
|||
} |
|||
|
|||
@Override |
|||
public boolean isCaseSensitive(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return true; // Assume case sensitive |
|||
} |
|||
|
|||
@Override |
|||
public boolean isSearchable(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return true; // All columns are searchable |
|||
} |
|||
|
|||
@Override |
|||
public boolean isCurrency(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return false; // No currency columns |
|||
} |
|||
|
|||
@Override |
|||
public int isNullable(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return columnNullable; // Assume nullable |
|||
} |
|||
|
|||
@Override |
|||
public boolean isSigned(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
// For simplicity, assume all numeric types are signed |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public int getColumnDisplaySize(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return 50; // Default display size |
|||
} |
|||
|
|||
@Override |
|||
public String getColumnLabel(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return columnNames.get(column - 1); |
|||
} |
|||
|
|||
@Override |
|||
public String getColumnName(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return columnNames.get(column - 1); |
|||
} |
|||
|
|||
@Override |
|||
public String getSchemaName(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return ""; // No schema concept in SeaweedFS |
|||
} |
|||
|
|||
@Override |
|||
public int getPrecision(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return 0; // Unknown precision |
|||
} |
|||
|
|||
@Override |
|||
public int getScale(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return 0; // Unknown scale |
|||
} |
|||
|
|||
@Override |
|||
public String getTableName(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return ""; // Table name not available in result set metadata |
|||
} |
|||
|
|||
@Override |
|||
public String getCatalogName(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return ""; // No catalog concept in SeaweedFS |
|||
} |
|||
|
|||
@Override |
|||
public int getColumnType(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
// For simplicity, we'll determine type based on column name patterns |
|||
String columnName = columnNames.get(column - 1).toLowerCase(); |
|||
|
|||
if (columnName.contains("timestamp") || columnName.contains("time") || columnName.equals("_timestamp_ns")) { |
|||
return Types.TIMESTAMP; |
|||
} else if (columnName.contains("id") || columnName.contains("count") || columnName.contains("size")) { |
|||
return Types.BIGINT; |
|||
} else if (columnName.contains("amount") || columnName.contains("price") || columnName.contains("rate")) { |
|||
return Types.DECIMAL; |
|||
} else if (columnName.contains("flag") || columnName.contains("enabled") || columnName.contains("active")) { |
|||
return Types.BOOLEAN; |
|||
} else { |
|||
return Types.VARCHAR; // Default to VARCHAR |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public String getColumnTypeName(int column) throws SQLException { |
|||
int sqlType = getColumnType(column); |
|||
switch (sqlType) { |
|||
case Types.VARCHAR: |
|||
return "VARCHAR"; |
|||
case Types.BIGINT: |
|||
return "BIGINT"; |
|||
case Types.DECIMAL: |
|||
return "DECIMAL"; |
|||
case Types.BOOLEAN: |
|||
return "BOOLEAN"; |
|||
case Types.TIMESTAMP: |
|||
return "TIMESTAMP"; |
|||
default: |
|||
return "VARCHAR"; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public boolean isReadOnly(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return true; // SeaweedFS is read-only |
|||
} |
|||
|
|||
@Override |
|||
public boolean isWritable(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return false; // SeaweedFS is read-only |
|||
} |
|||
|
|||
@Override |
|||
public boolean isDefinitelyWritable(int column) throws SQLException { |
|||
checkColumnIndex(column); |
|||
return false; // SeaweedFS is read-only |
|||
} |
|||
|
|||
@Override |
|||
public String getColumnClassName(int column) throws SQLException { |
|||
int sqlType = getColumnType(column); |
|||
switch (sqlType) { |
|||
case Types.VARCHAR: |
|||
return "java.lang.String"; |
|||
case Types.BIGINT: |
|||
return "java.lang.Long"; |
|||
case Types.DECIMAL: |
|||
return "java.math.BigDecimal"; |
|||
case Types.BOOLEAN: |
|||
return "java.lang.Boolean"; |
|||
case Types.TIMESTAMP: |
|||
return "java.sql.Timestamp"; |
|||
default: |
|||
return "java.lang.String"; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public <T> T unwrap(Class<T> iface) throws SQLException { |
|||
if (iface.isAssignableFrom(getClass())) { |
|||
return iface.cast(this); |
|||
} |
|||
throw new SQLException("Cannot unwrap to " + iface.getName()); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isWrapperFor(Class<?> iface) throws SQLException { |
|||
return iface.isAssignableFrom(getClass()); |
|||
} |
|||
|
|||
private void checkColumnIndex(int column) throws SQLException { |
|||
if (column < 1 || column > columnNames.size()) { |
|||
throw new SQLException("Column index " + column + " is out of range (1-" + columnNames.size() + ")"); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,389 @@ |
|||
package com.seaweedfs.jdbc; |
|||
|
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
|
|||
import java.sql.*; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* JDBC Statement implementation for SeaweedFS |
|||
*/ |
|||
public class SeaweedFSStatement implements Statement { |
|||
|
|||
private static final Logger logger = LoggerFactory.getLogger(SeaweedFSStatement.class); |
|||
|
|||
protected final SeaweedFSConnection connection; |
|||
private boolean closed = false; |
|||
private ResultSet currentResultSet = null; |
|||
private int updateCount = -1; |
|||
private int maxRows = 0; |
|||
private int queryTimeout = 0; |
|||
private int fetchSize = 1000; |
|||
private List<String> batch = new ArrayList<>(); |
|||
|
|||
public SeaweedFSStatement(SeaweedFSConnection connection) { |
|||
this.connection = connection; |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet executeQuery(String sql) throws SQLException { |
|||
checkClosed(); |
|||
logger.debug("Executing query: {}", sql); |
|||
|
|||
try { |
|||
// Send query to server |
|||
connection.sendMessage((byte)0x03, sql.getBytes()); // JDBC_MSG_EXECUTE_QUERY |
|||
|
|||
// Read response |
|||
SeaweedFSConnection.Response response = connection.readResponse(); |
|||
|
|||
if (response.type == (byte)0x01) { // JDBC_RESP_ERROR |
|||
throw new SQLException("Query failed: " + new String(response.data)); |
|||
} else if (response.type == (byte)0x02) { // JDBC_RESP_RESULT_SET |
|||
// Parse result set data |
|||
currentResultSet = new SeaweedFSResultSet(this, response.data); |
|||
updateCount = -1; |
|||
return currentResultSet; |
|||
} else { |
|||
throw new SQLException("Unexpected response type: " + response.type); |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
throw new SQLException("Failed to execute query: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public int executeUpdate(String sql) throws SQLException { |
|||
checkClosed(); |
|||
logger.debug("Executing update: {}", sql); |
|||
|
|||
try { |
|||
// Send update to server |
|||
connection.sendMessage((byte)0x04, sql.getBytes()); // JDBC_MSG_EXECUTE_UPDATE |
|||
|
|||
// Read response |
|||
SeaweedFSConnection.Response response = connection.readResponse(); |
|||
|
|||
if (response.type == (byte)0x01) { // JDBC_RESP_ERROR |
|||
throw new SQLException("Update failed: " + new String(response.data)); |
|||
} else if (response.type == (byte)0x03) { // JDBC_RESP_UPDATE_COUNT |
|||
// Parse update count |
|||
updateCount = parseUpdateCount(response.data); |
|||
currentResultSet = null; |
|||
return updateCount; |
|||
} else { |
|||
throw new SQLException("Unexpected response type: " + response.type); |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
throw new SQLException("Failed to execute update: " + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void close() throws SQLException { |
|||
if (!closed) { |
|||
if (currentResultSet != null) { |
|||
currentResultSet.close(); |
|||
currentResultSet = null; |
|||
} |
|||
closed = true; |
|||
logger.debug("Statement closed"); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxFieldSize() throws SQLException { |
|||
checkClosed(); |
|||
return 0; // No limit |
|||
} |
|||
|
|||
@Override |
|||
public void setMaxFieldSize(int max) throws SQLException { |
|||
checkClosed(); |
|||
// No-op |
|||
} |
|||
|
|||
@Override |
|||
public int getMaxRows() throws SQLException { |
|||
checkClosed(); |
|||
return maxRows; |
|||
} |
|||
|
|||
@Override |
|||
public void setMaxRows(int max) throws SQLException { |
|||
checkClosed(); |
|||
this.maxRows = max; |
|||
} |
|||
|
|||
@Override |
|||
public void setEscapeProcessing(boolean enable) throws SQLException { |
|||
checkClosed(); |
|||
// No-op |
|||
} |
|||
|
|||
@Override |
|||
public int getQueryTimeout() throws SQLException { |
|||
checkClosed(); |
|||
return queryTimeout; |
|||
} |
|||
|
|||
@Override |
|||
public void setQueryTimeout(int seconds) throws SQLException { |
|||
checkClosed(); |
|||
this.queryTimeout = seconds; |
|||
} |
|||
|
|||
@Override |
|||
public void cancel() throws SQLException { |
|||
checkClosed(); |
|||
// No-op - cancellation not supported |
|||
} |
|||
|
|||
@Override |
|||
public SQLWarning getWarnings() throws SQLException { |
|||
checkClosed(); |
|||
return null; |
|||
} |
|||
|
|||
@Override |
|||
public void clearWarnings() throws SQLException { |
|||
checkClosed(); |
|||
// No-op |
|||
} |
|||
|
|||
@Override |
|||
public void setCursorName(String name) throws SQLException { |
|||
checkClosed(); |
|||
// No-op - cursors not supported |
|||
} |
|||
|
|||
@Override |
|||
public boolean execute(String sql) throws SQLException { |
|||
checkClosed(); |
|||
logger.debug("Executing: {}", sql); |
|||
|
|||
// Determine if this is likely a query or update |
|||
String trimmedSql = sql.trim().toUpperCase(); |
|||
if (trimmedSql.startsWith("SELECT") || |
|||
trimmedSql.startsWith("SHOW") || |
|||
trimmedSql.startsWith("DESCRIBE") || |
|||
trimmedSql.startsWith("DESC") || |
|||
trimmedSql.startsWith("EXPLAIN")) { |
|||
// It's a query |
|||
executeQuery(sql); |
|||
return true; |
|||
} else { |
|||
// It's an update |
|||
executeUpdate(sql); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getResultSet() throws SQLException { |
|||
checkClosed(); |
|||
return currentResultSet; |
|||
} |
|||
|
|||
@Override |
|||
public int getUpdateCount() throws SQLException { |
|||
checkClosed(); |
|||
return updateCount; |
|||
} |
|||
|
|||
@Override |
|||
public boolean getMoreResults() throws SQLException { |
|||
checkClosed(); |
|||
if (currentResultSet != null) { |
|||
currentResultSet.close(); |
|||
currentResultSet = null; |
|||
} |
|||
updateCount = -1; |
|||
return false; // No more results |
|||
} |
|||
|
|||
@Override |
|||
public void setFetchDirection(int direction) throws SQLException { |
|||
checkClosed(); |
|||
if (direction != ResultSet.FETCH_FORWARD) { |
|||
throw new SQLException("Only FETCH_FORWARD is supported"); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public int getFetchDirection() throws SQLException { |
|||
checkClosed(); |
|||
return ResultSet.FETCH_FORWARD; |
|||
} |
|||
|
|||
@Override |
|||
public void setFetchSize(int rows) throws SQLException { |
|||
checkClosed(); |
|||
this.fetchSize = rows; |
|||
} |
|||
|
|||
@Override |
|||
public int getFetchSize() throws SQLException { |
|||
checkClosed(); |
|||
return fetchSize; |
|||
} |
|||
|
|||
@Override |
|||
public int getResultSetConcurrency() throws SQLException { |
|||
checkClosed(); |
|||
return ResultSet.CONCUR_READ_ONLY; |
|||
} |
|||
|
|||
@Override |
|||
public int getResultSetType() throws SQLException { |
|||
checkClosed(); |
|||
return ResultSet.TYPE_FORWARD_ONLY; |
|||
} |
|||
|
|||
@Override |
|||
public void addBatch(String sql) throws SQLException { |
|||
checkClosed(); |
|||
batch.add(sql); |
|||
} |
|||
|
|||
@Override |
|||
public void clearBatch() throws SQLException { |
|||
checkClosed(); |
|||
batch.clear(); |
|||
} |
|||
|
|||
@Override |
|||
public int[] executeBatch() throws SQLException { |
|||
checkClosed(); |
|||
int[] results = new int[batch.size()]; |
|||
|
|||
for (int i = 0; i < batch.size(); i++) { |
|||
try { |
|||
results[i] = executeUpdate(batch.get(i)); |
|||
} catch (SQLException e) { |
|||
results[i] = EXECUTE_FAILED; |
|||
} |
|||
} |
|||
|
|||
batch.clear(); |
|||
return results; |
|||
} |
|||
|
|||
@Override |
|||
public Connection getConnection() throws SQLException { |
|||
checkClosed(); |
|||
return connection; |
|||
} |
|||
|
|||
@Override |
|||
public boolean getMoreResults(int current) throws SQLException { |
|||
checkClosed(); |
|||
return getMoreResults(); |
|||
} |
|||
|
|||
@Override |
|||
public ResultSet getGeneratedKeys() throws SQLException { |
|||
throw new SQLFeatureNotSupportedException("Generated keys are not supported"); |
|||
} |
|||
|
|||
@Override |
|||
public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { |
|||
return executeUpdate(sql); |
|||
} |
|||
|
|||
@Override |
|||
public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { |
|||
return executeUpdate(sql); |
|||
} |
|||
|
|||
@Override |
|||
public int executeUpdate(String sql, String[] columnNames) throws SQLException { |
|||
return executeUpdate(sql); |
|||
} |
|||
|
|||
@Override |
|||
public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { |
|||
return execute(sql); |
|||
} |
|||
|
|||
@Override |
|||
public boolean execute(String sql, int[] columnIndexes) throws SQLException { |
|||
return execute(sql); |
|||
} |
|||
|
|||
@Override |
|||
public boolean execute(String sql, String[] columnNames) throws SQLException { |
|||
return execute(sql); |
|||
} |
|||
|
|||
@Override |
|||
public int getResultSetHoldability() throws SQLException { |
|||
checkClosed(); |
|||
return ResultSet.CLOSE_CURSORS_AT_COMMIT; |
|||
} |
|||
|
|||
@Override |
|||
public boolean isClosed() throws SQLException { |
|||
return closed; |
|||
} |
|||
|
|||
@Override |
|||
public void setPoolable(boolean poolable) throws SQLException { |
|||
checkClosed(); |
|||
// No-op |
|||
} |
|||
|
|||
@Override |
|||
public boolean isPoolable() throws SQLException { |
|||
checkClosed(); |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public void closeOnCompletion() throws SQLException { |
|||
checkClosed(); |
|||
// No-op |
|||
} |
|||
|
|||
@Override |
|||
public boolean isCloseOnCompletion() throws SQLException { |
|||
checkClosed(); |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public <T> T unwrap(Class<T> iface) throws SQLException { |
|||
if (iface.isAssignableFrom(getClass())) { |
|||
return iface.cast(this); |
|||
} |
|||
throw new SQLException("Cannot unwrap to " + iface.getName()); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isWrapperFor(Class<?> iface) throws SQLException { |
|||
return iface.isAssignableFrom(getClass()); |
|||
} |
|||
|
|||
protected void checkClosed() throws SQLException { |
|||
if (closed) { |
|||
throw new SQLException("Statement is closed"); |
|||
} |
|||
if (connection.isClosed()) { |
|||
throw new SQLException("Connection is closed"); |
|||
} |
|||
} |
|||
|
|||
private int parseUpdateCount(byte[] data) { |
|||
if (data.length >= 4) { |
|||
return ((data[0] & 0xFF) << 24) | |
|||
((data[1] & 0xFF) << 16) | |
|||
((data[2] & 0xFF) << 8) | |
|||
(data[3] & 0xFF); |
|||
} |
|||
return 0; |
|||
} |
|||
} |
@ -0,0 +1 @@ |
|||
com.seaweedfs.jdbc.SeaweedFSDriver |
@ -0,0 +1,75 @@ |
|||
package com.seaweedfs.jdbc; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import static org.junit.jupiter.api.Assertions.*; |
|||
|
|||
import java.sql.DriverManager; |
|||
import java.sql.SQLException; |
|||
|
|||
/** |
|||
* Basic tests for SeaweedFS JDBC driver |
|||
*/ |
|||
public class SeaweedFSDriverTest { |
|||
|
|||
@Test |
|||
public void testDriverRegistration() { |
|||
// Driver should be automatically registered via META-INF/services |
|||
assertDoesNotThrow(() -> { |
|||
Class.forName("com.seaweedfs.jdbc.SeaweedFSDriver"); |
|||
}); |
|||
} |
|||
|
|||
@Test |
|||
public void testURLAcceptance() throws SQLException { |
|||
SeaweedFSDriver driver = new SeaweedFSDriver(); |
|||
|
|||
// Valid URLs |
|||
assertTrue(driver.acceptsURL("jdbc:seaweedfs://localhost:8089/default")); |
|||
assertTrue(driver.acceptsURL("jdbc:seaweedfs://server:9000/test")); |
|||
assertTrue(driver.acceptsURL("jdbc:seaweedfs://192.168.1.100:8089/mydb")); |
|||
|
|||
// Invalid URLs |
|||
assertFalse(driver.acceptsURL("jdbc:mysql://localhost:3306/test")); |
|||
assertFalse(driver.acceptsURL("jdbc:postgresql://localhost:5432/test")); |
|||
assertFalse(driver.acceptsURL(null)); |
|||
assertFalse(driver.acceptsURL("")); |
|||
assertFalse(driver.acceptsURL("not-a-url")); |
|||
} |
|||
|
|||
@Test |
|||
public void testDriverInfo() { |
|||
SeaweedFSDriver driver = new SeaweedFSDriver(); |
|||
|
|||
assertEquals(SeaweedFSDriver.DRIVER_MAJOR_VERSION, driver.getMajorVersion()); |
|||
assertEquals(SeaweedFSDriver.DRIVER_MINOR_VERSION, driver.getMinorVersion()); |
|||
assertFalse(driver.jdbcCompliant()); // We're not fully JDBC compliant |
|||
|
|||
assertNotNull(SeaweedFSDriver.getDriverInfo()); |
|||
assertTrue(SeaweedFSDriver.getDriverInfo().contains("SeaweedFS")); |
|||
assertTrue(SeaweedFSDriver.getDriverInfo().contains("JDBC")); |
|||
} |
|||
|
|||
@Test |
|||
public void testPropertyInfo() throws SQLException { |
|||
SeaweedFSDriver driver = new SeaweedFSDriver(); |
|||
|
|||
var properties = driver.getPropertyInfo("jdbc:seaweedfs://localhost:8089/default", null); |
|||
assertNotNull(properties); |
|||
assertTrue(properties.length > 0); |
|||
|
|||
// Check that basic properties are present |
|||
boolean foundHost = false, foundPort = false, foundDatabase = false; |
|||
for (var prop : properties) { |
|||
if ("host".equals(prop.name)) foundHost = true; |
|||
if ("port".equals(prop.name)) foundPort = true; |
|||
if ("database".equals(prop.name)) foundDatabase = true; |
|||
} |
|||
|
|||
assertTrue(foundHost, "Host property should be present"); |
|||
assertTrue(foundPort, "Port property should be present"); |
|||
assertTrue(foundDatabase, "Database property should be present"); |
|||
} |
|||
|
|||
// Note: Connection tests would require a running SeaweedFS JDBC server |
|||
// These tests would be part of integration tests, not unit tests |
|||
} |
@ -0,0 +1,141 @@ |
|||
package command |
|||
|
|||
import ( |
|||
"context" |
|||
"fmt" |
|||
"os" |
|||
"os/signal" |
|||
"syscall" |
|||
"time" |
|||
|
|||
weed_server "github.com/seaweedfs/seaweedfs/weed/server" |
|||
"github.com/seaweedfs/seaweedfs/weed/util" |
|||
) |
|||
|
|||
var ( |
|||
jdbcOptions JdbcOptions |
|||
) |
|||
|
|||
type JdbcOptions struct { |
|||
host *string |
|||
port *int |
|||
masterAddr *string |
|||
} |
|||
|
|||
func init() { |
|||
cmdJdbc.Run = runJdbc // break init cycle
|
|||
jdbcOptions.host = cmdJdbc.Flag.String("host", "localhost", "JDBC server host") |
|||
jdbcOptions.port = cmdJdbc.Flag.Int("port", 8089, "JDBC server port") |
|||
jdbcOptions.masterAddr = cmdJdbc.Flag.String("master", "localhost:9333", "SeaweedFS master server address") |
|||
} |
|||
|
|||
var cmdJdbc = &Command{ |
|||
UsageLine: "jdbc -port=8089 -master=<master_server>", |
|||
Short: "start a JDBC server for SQL queries", |
|||
Long: `Start a JDBC server that provides SQL query access to SeaweedFS. |
|||
|
|||
This JDBC server allows standard JDBC clients and tools to connect to SeaweedFS |
|||
and execute SQL queries against MQ topics. It implements a subset of the JDBC |
|||
protocol for compatibility with most database tools and applications. |
|||
|
|||
Examples: |
|||
|
|||
# Start JDBC server on default port 8089 |
|||
weed jdbc |
|||
|
|||
# Start on custom port with specific master |
|||
weed jdbc -port=8090 -master=master1:9333 |
|||
|
|||
# Allow connections from any host |
|||
weed jdbc -host=0.0.0.0 -port=8089 |
|||
|
|||
Clients can then connect using JDBC URL: |
|||
jdbc:seaweedfs://hostname:port/database
|
|||
|
|||
Supported SQL operations: |
|||
- SELECT queries on MQ topics |
|||
- DESCRIBE/DESC commands |
|||
- SHOW DATABASES/TABLES commands |
|||
- Aggregation functions (COUNT, SUM, AVG, MIN, MAX) |
|||
- WHERE clauses with filtering |
|||
- System columns (_timestamp_ns, _key, _source) |
|||
|
|||
Compatible with: |
|||
- Standard JDBC tools (DBeaver, IntelliJ DataGrip, etc.) |
|||
- Business Intelligence tools (Tableau, Power BI, etc.) |
|||
- Java applications using JDBC drivers |
|||
- SQL reporting tools |
|||
|
|||
`, |
|||
} |
|||
|
|||
func runJdbc(cmd *Command, args []string) bool { |
|||
|
|||
util.LoadConfiguration("security", false) |
|||
|
|||
// Validate options
|
|||
if *jdbcOptions.masterAddr == "" { |
|||
fmt.Fprintf(os.Stderr, "Error: master address is required\n") |
|||
return false |
|||
} |
|||
|
|||
// Create JDBC server
|
|||
jdbcServer, err := weed_server.NewJDBCServer(*jdbcOptions.host, *jdbcOptions.port, *jdbcOptions.masterAddr) |
|||
if err != nil { |
|||
fmt.Fprintf(os.Stderr, "Error creating JDBC server: %v\n", err) |
|||
return false |
|||
} |
|||
|
|||
// Start the server
|
|||
fmt.Printf("Starting SeaweedFS JDBC Server...\n") |
|||
fmt.Printf("Host: %s\n", *jdbcOptions.host) |
|||
fmt.Printf("Port: %d\n", *jdbcOptions.port) |
|||
fmt.Printf("Master: %s\n", *jdbcOptions.masterAddr) |
|||
fmt.Printf("\nJDBC URL: jdbc:seaweedfs://%s:%d/default\n", *jdbcOptions.host, *jdbcOptions.port) |
|||
fmt.Printf("\nSupported operations:\n") |
|||
fmt.Printf(" - SELECT queries on MQ topics\n") |
|||
fmt.Printf(" - DESCRIBE/DESC table_name\n") |
|||
fmt.Printf(" - SHOW DATABASES\n") |
|||
fmt.Printf(" - SHOW TABLES\n") |
|||
fmt.Printf(" - Aggregations: COUNT, SUM, AVG, MIN, MAX\n") |
|||
fmt.Printf(" - System columns: _timestamp_ns, _key, _source\n") |
|||
fmt.Printf("\nReady for JDBC connections!\n\n") |
|||
|
|||
err = jdbcServer.Start() |
|||
if err != nil { |
|||
fmt.Fprintf(os.Stderr, "Error starting JDBC server: %v\n", err) |
|||
return false |
|||
} |
|||
|
|||
// Set up signal handling for graceful shutdown
|
|||
sigChan := make(chan os.Signal, 1) |
|||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) |
|||
|
|||
// Wait for shutdown signal
|
|||
<-sigChan |
|||
fmt.Printf("\nReceived shutdown signal, stopping JDBC server...\n") |
|||
|
|||
// Create context with timeout for graceful shutdown
|
|||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
|||
defer cancel() |
|||
|
|||
// Stop the server with timeout
|
|||
done := make(chan error, 1) |
|||
go func() { |
|||
done <- jdbcServer.Stop() |
|||
}() |
|||
|
|||
select { |
|||
case err := <-done: |
|||
if err != nil { |
|||
fmt.Fprintf(os.Stderr, "Error stopping JDBC server: %v\n", err) |
|||
return false |
|||
} |
|||
fmt.Printf("JDBC server stopped successfully\n") |
|||
case <-ctx.Done(): |
|||
fmt.Fprintf(os.Stderr, "Timeout waiting for JDBC server to stop\n") |
|||
return false |
|||
} |
|||
|
|||
return true |
|||
} |
@ -0,0 +1,524 @@ |
|||
package weed_server |
|||
|
|||
import ( |
|||
"bufio" |
|||
"context" |
|||
"encoding/binary" |
|||
"fmt" |
|||
"io" |
|||
"net" |
|||
"sync" |
|||
"time" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
"github.com/seaweedfs/seaweedfs/weed/query/engine" |
|||
"github.com/seaweedfs/seaweedfs/weed/query/sqltypes" |
|||
) |
|||
|
|||
// JDBCServer provides JDBC-compatible access to SeaweedFS SQL engine
|
|||
type JDBCServer struct { |
|||
host string |
|||
port int |
|||
masterAddr string |
|||
listener net.Listener |
|||
sqlEngine *engine.SQLEngine |
|||
connections map[net.Conn]*JDBCConnection |
|||
connMutex sync.RWMutex |
|||
shutdown chan struct{} |
|||
wg sync.WaitGroup |
|||
} |
|||
|
|||
// JDBCConnection represents a single JDBC client connection
|
|||
type JDBCConnection struct { |
|||
conn net.Conn |
|||
reader *bufio.Reader |
|||
writer *bufio.Writer |
|||
database string |
|||
autoCommit bool |
|||
connectionID uint32 |
|||
closed bool |
|||
mutex sync.Mutex |
|||
} |
|||
|
|||
// JDBC Protocol Constants
|
|||
const ( |
|||
// Message Types
|
|||
JDBC_MSG_CONNECT = 0x01 |
|||
JDBC_MSG_DISCONNECT = 0x02 |
|||
JDBC_MSG_EXECUTE_QUERY = 0x03 |
|||
JDBC_MSG_EXECUTE_UPDATE = 0x04 |
|||
JDBC_MSG_PREPARE = 0x05 |
|||
JDBC_MSG_EXECUTE_PREP = 0x06 |
|||
JDBC_MSG_GET_METADATA = 0x07 |
|||
JDBC_MSG_SET_AUTOCOMMIT = 0x08 |
|||
JDBC_MSG_COMMIT = 0x09 |
|||
JDBC_MSG_ROLLBACK = 0x0A |
|||
|
|||
// Response Types
|
|||
JDBC_RESP_OK = 0x00 |
|||
JDBC_RESP_ERROR = 0x01 |
|||
JDBC_RESP_RESULT_SET = 0x02 |
|||
JDBC_RESP_UPDATE_COUNT = 0x03 |
|||
JDBC_RESP_METADATA = 0x04 |
|||
|
|||
// Default values
|
|||
DEFAULT_JDBC_PORT = 8089 |
|||
) |
|||
|
|||
// NewJDBCServer creates a new JDBC server instance
|
|||
func NewJDBCServer(host string, port int, masterAddr string) (*JDBCServer, error) { |
|||
if port <= 0 { |
|||
port = DEFAULT_JDBC_PORT |
|||
} |
|||
if host == "" { |
|||
host = "localhost" |
|||
} |
|||
|
|||
// Create SQL engine
|
|||
sqlEngine := engine.NewSQLEngine(masterAddr) |
|||
|
|||
server := &JDBCServer{ |
|||
host: host, |
|||
port: port, |
|||
masterAddr: masterAddr, |
|||
sqlEngine: sqlEngine, |
|||
connections: make(map[net.Conn]*JDBCConnection), |
|||
shutdown: make(chan struct{}), |
|||
} |
|||
|
|||
return server, nil |
|||
} |
|||
|
|||
// Start begins listening for JDBC connections
|
|||
func (s *JDBCServer) Start() error { |
|||
addr := fmt.Sprintf("%s:%d", s.host, s.port) |
|||
listener, err := net.Listen("tcp", addr) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to start JDBC server on %s: %v", addr, err) |
|||
} |
|||
|
|||
s.listener = listener |
|||
glog.Infof("JDBC Server listening on %s", addr) |
|||
|
|||
s.wg.Add(1) |
|||
go s.acceptConnections() |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// Stop gracefully shuts down the JDBC server
|
|||
func (s *JDBCServer) Stop() error { |
|||
close(s.shutdown) |
|||
|
|||
if s.listener != nil { |
|||
s.listener.Close() |
|||
} |
|||
|
|||
// Close all connections
|
|||
s.connMutex.Lock() |
|||
for conn, jdbcConn := range s.connections { |
|||
jdbcConn.close() |
|||
conn.Close() |
|||
} |
|||
s.connections = make(map[net.Conn]*JDBCConnection) |
|||
s.connMutex.Unlock() |
|||
|
|||
s.wg.Wait() |
|||
glog.Infof("JDBC Server stopped") |
|||
return nil |
|||
} |
|||
|
|||
// acceptConnections handles incoming JDBC connections
|
|||
func (s *JDBCServer) acceptConnections() { |
|||
defer s.wg.Done() |
|||
|
|||
for { |
|||
select { |
|||
case <-s.shutdown: |
|||
return |
|||
default: |
|||
} |
|||
|
|||
conn, err := s.listener.Accept() |
|||
if err != nil { |
|||
select { |
|||
case <-s.shutdown: |
|||
return |
|||
default: |
|||
glog.Errorf("Failed to accept JDBC connection: %v", err) |
|||
continue |
|||
} |
|||
} |
|||
|
|||
s.wg.Add(1) |
|||
go s.handleConnection(conn) |
|||
} |
|||
} |
|||
|
|||
// handleConnection processes a single JDBC connection
|
|||
func (s *JDBCServer) handleConnection(conn net.Conn) { |
|||
defer s.wg.Done() |
|||
defer conn.Close() |
|||
|
|||
// Create JDBC connection wrapper
|
|||
jdbcConn := &JDBCConnection{ |
|||
conn: conn, |
|||
reader: bufio.NewReader(conn), |
|||
writer: bufio.NewWriter(conn), |
|||
database: "default", |
|||
autoCommit: true, |
|||
connectionID: s.generateConnectionID(), |
|||
} |
|||
|
|||
// Register connection
|
|||
s.connMutex.Lock() |
|||
s.connections[conn] = jdbcConn |
|||
s.connMutex.Unlock() |
|||
|
|||
// Clean up on exit
|
|||
defer func() { |
|||
s.connMutex.Lock() |
|||
delete(s.connections, conn) |
|||
s.connMutex.Unlock() |
|||
}() |
|||
|
|||
glog.Infof("New JDBC connection from %s (ID: %d)", conn.RemoteAddr(), jdbcConn.connectionID) |
|||
|
|||
// Handle connection messages
|
|||
for { |
|||
select { |
|||
case <-s.shutdown: |
|||
return |
|||
default: |
|||
} |
|||
|
|||
// Set read timeout
|
|||
conn.SetReadDeadline(time.Now().Add(30 * time.Second)) |
|||
|
|||
err := s.handleMessage(jdbcConn) |
|||
if err != nil { |
|||
if err == io.EOF { |
|||
glog.Infof("JDBC client disconnected (ID: %d)", jdbcConn.connectionID) |
|||
} else { |
|||
glog.Errorf("Error handling JDBC message (ID: %d): %v", jdbcConn.connectionID, err) |
|||
} |
|||
return |
|||
} |
|||
} |
|||
} |
|||
|
|||
// handleMessage processes a single JDBC protocol message
|
|||
func (s *JDBCServer) handleMessage(conn *JDBCConnection) error { |
|||
// Read message header (message type + length)
|
|||
header := make([]byte, 5) |
|||
_, err := io.ReadFull(conn.reader, header) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
msgType := header[0] |
|||
msgLength := binary.BigEndian.Uint32(header[1:5]) |
|||
|
|||
// Read message body
|
|||
msgBody := make([]byte, msgLength) |
|||
if msgLength > 0 { |
|||
_, err = io.ReadFull(conn.reader, msgBody) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
} |
|||
|
|||
// Process message based on type
|
|||
switch msgType { |
|||
case JDBC_MSG_CONNECT: |
|||
return s.handleConnect(conn, msgBody) |
|||
case JDBC_MSG_DISCONNECT: |
|||
return s.handleDisconnect(conn) |
|||
case JDBC_MSG_EXECUTE_QUERY: |
|||
return s.handleExecuteQuery(conn, msgBody) |
|||
case JDBC_MSG_EXECUTE_UPDATE: |
|||
return s.handleExecuteUpdate(conn, msgBody) |
|||
case JDBC_MSG_GET_METADATA: |
|||
return s.handleGetMetadata(conn, msgBody) |
|||
case JDBC_MSG_SET_AUTOCOMMIT: |
|||
return s.handleSetAutoCommit(conn, msgBody) |
|||
case JDBC_MSG_COMMIT: |
|||
return s.handleCommit(conn) |
|||
case JDBC_MSG_ROLLBACK: |
|||
return s.handleRollback(conn) |
|||
default: |
|||
return s.sendError(conn, fmt.Errorf("unknown message type: %d", msgType)) |
|||
} |
|||
} |
|||
|
|||
// handleConnect processes JDBC connection request
|
|||
func (s *JDBCServer) handleConnect(conn *JDBCConnection, msgBody []byte) error { |
|||
// Parse connection string (database name)
|
|||
if len(msgBody) > 0 { |
|||
conn.database = string(msgBody) |
|||
} |
|||
|
|||
glog.Infof("JDBC client connected to database: %s (ID: %d)", conn.database, conn.connectionID) |
|||
|
|||
// Send OK response
|
|||
return s.sendOK(conn, "Connected successfully") |
|||
} |
|||
|
|||
// handleDisconnect processes JDBC disconnect request
|
|||
func (s *JDBCServer) handleDisconnect(conn *JDBCConnection) error { |
|||
glog.Infof("JDBC client disconnecting (ID: %d)", conn.connectionID) |
|||
conn.close() |
|||
return io.EOF // This will cause the connection handler to exit
|
|||
} |
|||
|
|||
// handleExecuteQuery processes SQL SELECT queries
|
|||
func (s *JDBCServer) handleExecuteQuery(conn *JDBCConnection, msgBody []byte) error { |
|||
sql := string(msgBody) |
|||
|
|||
glog.V(2).Infof("Executing query (ID: %d): %s", conn.connectionID, sql) |
|||
|
|||
// Execute SQL using the query engine
|
|||
ctx := context.Background() |
|||
result, err := s.sqlEngine.ExecuteSQL(ctx, sql) |
|||
if err != nil { |
|||
return s.sendError(conn, err) |
|||
} |
|||
|
|||
if result.Error != nil { |
|||
return s.sendError(conn, result.Error) |
|||
} |
|||
|
|||
// Send result set
|
|||
return s.sendResultSet(conn, result) |
|||
} |
|||
|
|||
// handleExecuteUpdate processes SQL UPDATE/INSERT/DELETE queries
|
|||
func (s *JDBCServer) handleExecuteUpdate(conn *JDBCConnection, msgBody []byte) error { |
|||
sql := string(msgBody) |
|||
|
|||
glog.V(2).Infof("Executing update (ID: %d): %s", conn.connectionID, sql) |
|||
|
|||
// For now, treat updates same as queries since SeaweedFS SQL is read-only
|
|||
ctx := context.Background() |
|||
result, err := s.sqlEngine.ExecuteSQL(ctx, sql) |
|||
if err != nil { |
|||
return s.sendError(conn, err) |
|||
} |
|||
|
|||
if result.Error != nil { |
|||
return s.sendError(conn, result.Error) |
|||
} |
|||
|
|||
// Send update count (0 for read-only operations)
|
|||
return s.sendUpdateCount(conn, 0) |
|||
} |
|||
|
|||
// handleGetMetadata processes JDBC metadata requests
|
|||
func (s *JDBCServer) handleGetMetadata(conn *JDBCConnection, msgBody []byte) error { |
|||
metadataType := string(msgBody) |
|||
|
|||
glog.V(2).Infof("Getting metadata (ID: %d): %s", conn.connectionID, metadataType) |
|||
|
|||
switch metadataType { |
|||
case "tables": |
|||
return s.sendTablesMetadata(conn) |
|||
case "databases", "schemas": |
|||
return s.sendDatabasesMetadata(conn) |
|||
default: |
|||
return s.sendError(conn, fmt.Errorf("unsupported metadata type: %s", metadataType)) |
|||
} |
|||
} |
|||
|
|||
// handleSetAutoCommit processes autocommit setting
|
|||
func (s *JDBCServer) handleSetAutoCommit(conn *JDBCConnection, msgBody []byte) error { |
|||
autoCommit := len(msgBody) > 0 && msgBody[0] == 1 |
|||
conn.autoCommit = autoCommit |
|||
|
|||
glog.V(2).Infof("Setting autocommit (ID: %d): %v", conn.connectionID, autoCommit) |
|||
|
|||
return s.sendOK(conn, fmt.Sprintf("AutoCommit set to %v", autoCommit)) |
|||
} |
|||
|
|||
// handleCommit processes transaction commit (no-op for read-only)
|
|||
func (s *JDBCServer) handleCommit(conn *JDBCConnection) error { |
|||
glog.V(2).Infof("Commit (ID: %d): no-op for read-only", conn.connectionID) |
|||
return s.sendOK(conn, "Commit successful") |
|||
} |
|||
|
|||
// handleRollback processes transaction rollback (no-op for read-only)
|
|||
func (s *JDBCServer) handleRollback(conn *JDBCConnection) error { |
|||
glog.V(2).Infof("Rollback (ID: %d): no-op for read-only", conn.connectionID) |
|||
return s.sendOK(conn, "Rollback successful") |
|||
} |
|||
|
|||
// sendOK sends a success response
|
|||
func (s *JDBCServer) sendOK(conn *JDBCConnection, message string) error { |
|||
return s.sendResponse(conn, JDBC_RESP_OK, []byte(message)) |
|||
} |
|||
|
|||
// sendError sends an error response
|
|||
func (s *JDBCServer) sendError(conn *JDBCConnection, err error) error { |
|||
return s.sendResponse(conn, JDBC_RESP_ERROR, []byte(err.Error())) |
|||
} |
|||
|
|||
// sendResultSet sends query results
|
|||
func (s *JDBCServer) sendResultSet(conn *JDBCConnection, result *engine.QueryResult) error { |
|||
// Serialize result set
|
|||
data := s.serializeResultSet(result) |
|||
return s.sendResponse(conn, JDBC_RESP_RESULT_SET, data) |
|||
} |
|||
|
|||
// sendUpdateCount sends update operation result
|
|||
func (s *JDBCServer) sendUpdateCount(conn *JDBCConnection, count int) error { |
|||
data := make([]byte, 4) |
|||
binary.BigEndian.PutUint32(data, uint32(count)) |
|||
return s.sendResponse(conn, JDBC_RESP_UPDATE_COUNT, data) |
|||
} |
|||
|
|||
// sendTablesMetadata sends table metadata
|
|||
func (s *JDBCServer) sendTablesMetadata(conn *JDBCConnection) error { |
|||
// For now, return empty metadata - this would need to query the schema catalog
|
|||
data := s.serializeTablesMetadata([]string{}) |
|||
return s.sendResponse(conn, JDBC_RESP_METADATA, data) |
|||
} |
|||
|
|||
// sendDatabasesMetadata sends database/schema metadata
|
|||
func (s *JDBCServer) sendDatabasesMetadata(conn *JDBCConnection) error { |
|||
// Return default databases
|
|||
databases := []string{"default", "test"} |
|||
data := s.serializeDatabasesMetadata(databases) |
|||
return s.sendResponse(conn, JDBC_RESP_METADATA, data) |
|||
} |
|||
|
|||
// sendResponse sends a response with the given type and data
|
|||
func (s *JDBCServer) sendResponse(conn *JDBCConnection, responseType byte, data []byte) error { |
|||
conn.mutex.Lock() |
|||
defer conn.mutex.Unlock() |
|||
|
|||
// Write response header
|
|||
header := make([]byte, 5) |
|||
header[0] = responseType |
|||
binary.BigEndian.PutUint32(header[1:5], uint32(len(data))) |
|||
|
|||
_, err := conn.writer.Write(header) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// Write response data
|
|||
if len(data) > 0 { |
|||
_, err = conn.writer.Write(data) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
} |
|||
|
|||
return conn.writer.Flush() |
|||
} |
|||
|
|||
// serializeResultSet converts QueryResult to JDBC wire format
|
|||
func (s *JDBCServer) serializeResultSet(result *engine.QueryResult) []byte { |
|||
var data []byte |
|||
|
|||
// Column count
|
|||
colCount := make([]byte, 4) |
|||
binary.BigEndian.PutUint32(colCount, uint32(len(result.Columns))) |
|||
data = append(data, colCount...) |
|||
|
|||
// Column names
|
|||
for _, col := range result.Columns { |
|||
colName := []byte(col) |
|||
colLen := make([]byte, 4) |
|||
binary.BigEndian.PutUint32(colLen, uint32(len(colName))) |
|||
data = append(data, colLen...) |
|||
data = append(data, colName...) |
|||
} |
|||
|
|||
// Row count
|
|||
rowCount := make([]byte, 4) |
|||
binary.BigEndian.PutUint32(rowCount, uint32(len(result.Rows))) |
|||
data = append(data, rowCount...) |
|||
|
|||
// Rows
|
|||
for _, row := range result.Rows { |
|||
for _, value := range row { |
|||
// Convert value to string and serialize
|
|||
valueStr := s.valueToString(value) |
|||
valueBytes := []byte(valueStr) |
|||
valueLen := make([]byte, 4) |
|||
binary.BigEndian.PutUint32(valueLen, uint32(len(valueBytes))) |
|||
data = append(data, valueLen...) |
|||
data = append(data, valueBytes...) |
|||
} |
|||
} |
|||
|
|||
return data |
|||
} |
|||
|
|||
// serializeTablesMetadata converts table list to wire format
|
|||
func (s *JDBCServer) serializeTablesMetadata(tables []string) []byte { |
|||
var data []byte |
|||
|
|||
// Table count
|
|||
tableCount := make([]byte, 4) |
|||
binary.BigEndian.PutUint32(tableCount, uint32(len(tables))) |
|||
data = append(data, tableCount...) |
|||
|
|||
// Table names
|
|||
for _, table := range tables { |
|||
tableBytes := []byte(table) |
|||
tableLen := make([]byte, 4) |
|||
binary.BigEndian.PutUint32(tableLen, uint32(len(tableBytes))) |
|||
data = append(data, tableLen...) |
|||
data = append(data, tableBytes...) |
|||
} |
|||
|
|||
return data |
|||
} |
|||
|
|||
// serializeDatabasesMetadata converts database list to wire format
|
|||
func (s *JDBCServer) serializeDatabasesMetadata(databases []string) []byte { |
|||
var data []byte |
|||
|
|||
// Database count
|
|||
dbCount := make([]byte, 4) |
|||
binary.BigEndian.PutUint32(dbCount, uint32(len(databases))) |
|||
data = append(data, dbCount...) |
|||
|
|||
// Database names
|
|||
for _, db := range databases { |
|||
dbBytes := []byte(db) |
|||
dbLen := make([]byte, 4) |
|||
binary.BigEndian.PutUint32(dbLen, uint32(len(dbBytes))) |
|||
data = append(data, dbLen...) |
|||
data = append(data, dbBytes...) |
|||
} |
|||
|
|||
return data |
|||
} |
|||
|
|||
// valueToString converts a sqltypes.Value to string representation
|
|||
func (s *JDBCServer) valueToString(value sqltypes.Value) string { |
|||
if value.IsNull() { |
|||
return "" |
|||
} |
|||
|
|||
return value.ToString() |
|||
} |
|||
|
|||
// generateConnectionID generates a unique connection ID
|
|||
func (s *JDBCServer) generateConnectionID() uint32 { |
|||
return uint32(time.Now().UnixNano() % 1000000) |
|||
} |
|||
|
|||
// close marks the connection as closed
|
|||
func (c *JDBCConnection) close() { |
|||
c.mutex.Lock() |
|||
defer c.mutex.Unlock() |
|||
c.closed = true |
|||
} |
|||
|
|||
// GetAddress returns the server address
|
|||
func (s *JDBCServer) GetAddress() string { |
|||
return fmt.Sprintf("%s:%d", s.host, s.port) |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue