Merge 'bindings/java: Implement JDBC4DatabaseMetadata getTables ' from Kim Seon Woo

## Purpose
Implement `getTables` which is used to extract metadata about the
database
## Changes
- Implement `JDBC4DatabaseMetaData's` `getTables` method
- Extract `JDBC4ResultSet` as field in `JDBC4PreparedStatement`
## Related Issue
https://github.com/tursodatabase/limbo/issues/615

Closes #1687
This commit is contained in:
Pekka Enberg 2025-06-09 10:46:18 +03:00
commit ae2e0bd71c
5 changed files with 204 additions and 11 deletions

View file

@ -119,8 +119,17 @@ public final class LimboResultSet {
this.open = false;
}
// Note that columnIndex starts from 1
@Nullable
public Object get(String columnName) throws SQLException {
final int columnsLength = this.columnNames.length;
for (int i = 0; i < columnsLength; i++) {
if (this.columnNames[i].equals(columnName)) {
return get(i + 1);
}
}
throw new SQLException("column name " + columnName + " not found");
}
public Object get(int columnIndex) throws SQLException {
if (!this.isOpen()) {
throw new SQLException("ResultSet is not open");

View file

@ -687,16 +687,85 @@ public final class JDBC4DatabaseMetaData implements DatabaseMetaData {
return null;
}
// TODO: make use of getSearchStringEscape
@Override
@SkipNullableCheck
public ResultSet getTables(
@Nullable String catalog,
@Nullable String schemaPattern,
String tableNamePattern,
@Nullable String[] types)
throws SQLException {
// TODO: after union is supported
return null;
// SQLite doesn't support catalogs or schemas reject if non-empty values provided
if (catalog != null && !catalog.isEmpty()) {
return connection.prepareStatement("SELECT * FROM sqlite_schema WHERE 1=0").executeQuery();
}
if (schemaPattern != null && !schemaPattern.isEmpty()) {
return connection.prepareStatement("SELECT * FROM sqlite_schema WHERE 1=0").executeQuery();
}
// Start building query
StringBuilder sql =
new StringBuilder(
"SELECT "
+ "NULL AS TABLE_CAT, "
+ "NULL AS TABLE_SCHEM, "
+ "name AS TABLE_NAME, "
+ "CASE type "
+ " WHEN 'table' THEN 'TABLE' "
+ " WHEN 'view' THEN 'VIEW' "
+ " ELSE UPPER(type) "
+ "END AS TABLE_TYPE, "
+ "NULL AS REMARKS, "
+ "NULL AS TYPE_CAT, "
+ "NULL AS TYPE_SCHEM, "
+ "NULL AS TYPE_NAME, "
+ "NULL AS SELF_REFERENCING_COL_NAME, "
+ "NULL AS REF_GENERATION "
+ "FROM sqlite_schema "
+ "WHERE 1=1");
// Apply type filtering if needed
if (types != null && types.length > 0) {
sql.append(" AND type IN (");
for (int i = 0; i < types.length; i++) {
if (i > 0) sql.append(", ");
sql.append("?");
}
sql.append(")");
}
// Apply table name pattern filtering
if (tableNamePattern != null) {
sql.append(" AND name LIKE ?");
}
// Comply with spec: sort by TABLE_TYPE, TABLE_CAT, TABLE_SCHEM, TABLE_NAME
sql.append(" ORDER BY TABLE_TYPE, TABLE_CAT, TABLE_SCHEM, TABLE_NAME");
// Prepare and bind statement
PreparedStatement stmt = connection.prepareStatement(sql.toString());
int paramIndex = 1;
if (types != null && types.length > 0) {
for (String type : types) {
String sqliteType;
if ("TABLE".equalsIgnoreCase(type)) {
sqliteType = "table";
} else if ("VIEW".equalsIgnoreCase(type)) {
sqliteType = "view";
} else {
sqliteType = type.toLowerCase();
}
stmt.setString(paramIndex++, sqliteType);
}
}
if (tableNamePattern != null) {
stmt.setString(paramIndex, tableNamePattern);
}
return stmt.executeQuery();
}
@Override

View file

@ -28,6 +28,7 @@ import tech.turso.core.LimboResultSet;
public final class JDBC4PreparedStatement extends JDBC4Statement implements PreparedStatement {
private final String sql;
private final JDBC4ResultSet resultSet;
public JDBC4PreparedStatement(JDBC4Connection connection, String sql) throws SQLException {
super(connection);
@ -35,13 +36,13 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep
this.sql = sql;
this.statement = connection.prepare(sql);
this.statement.initializeColumnMetadata();
this.resultSet = new JDBC4ResultSet(this.statement.getResultSet());
}
@Override
public ResultSet executeQuery() throws SQLException {
// TODO: check bindings etc
requireNonNull(this.statement);
return new JDBC4ResultSet(this.statement.getResultSet());
return this.resultSet;
}
@Override
@ -202,9 +203,8 @@ public final class JDBC4PreparedStatement extends JDBC4Statement implements Prep
}
@Override
@SkipNullableCheck
public ResultSetMetaData getMetaData() throws SQLException {
return null;
return this.resultSet;
}
@Override

View file

@ -190,8 +190,12 @@ public final class JDBC4ResultSet implements ResultSet, ResultSetMetaData {
@Override
public String getString(String columnLabel) throws SQLException {
// TODO
return "";
final Object result = this.resultSet.get(columnLabel);
if (result == null) {
return "";
}
return wrapTypeConversion(() -> (String) result);
}
@Override

View file

@ -1,9 +1,16 @@
package tech.turso.jdbc4;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import tech.turso.TestUtils;
@ -44,4 +51,108 @@ class JDBC4DatabaseMetaDataTest {
void getDriverVersion_should_not_return_empty_string() {
assertFalse(metaData.getDriverVersion().isEmpty());
}
@Test
void getTables_with_non_empty_catalog_should_return_empty() throws SQLException {
ResultSet rs = metaData.getTables("nonexistent", null, null, null);
assertNotNull(rs);
assertFalse(rs.next());
rs.close();
}
@Test
void getTables_with_non_empty_schema_should_return_empty() throws SQLException {
ResultSet rs = metaData.getTables(null, "schema", null, null);
assertNotNull(rs);
assertFalse(rs.next());
rs.close();
}
@Test
void getTables_should_return_correct_table_info() throws SQLException {
try (Statement stmt = connection.createStatement()) {
stmt.execute("CREATE TABLE test_table (id INTEGER PRIMARY KEY)");
}
ResultSet rs = metaData.getTables(null, null, null, null);
assertNotNull(rs);
assertTrue(rs.next());
assertEquals("test_table", rs.getString("TABLE_NAME"));
assertEquals("TABLE", rs.getString("TABLE_TYPE"));
assertFalse(rs.next());
rs.close();
}
@Test
void getTables_with_pattern_should_filter_results() throws SQLException {
// Create test tables
try (Statement stmt = connection.createStatement()) {
stmt.execute("CREATE TABLE test1 (id INTEGER PRIMARY KEY)");
stmt.execute("CREATE TABLE test2 (id INTEGER PRIMARY KEY)");
stmt.execute("CREATE TABLE other (id INTEGER PRIMARY KEY)");
}
ResultSet rs = metaData.getTables(null, null, "test%", null);
assertNotNull(rs);
int tableCount = 0;
while (rs.next()) {
String tableName = rs.getString("TABLE_NAME");
assertTrue(tableName.startsWith("test"));
tableCount++;
}
assertEquals(2, tableCount);
rs.close();
}
@Test
@Disabled("CREATE VIEW not supported yet")
void getTables_with_type_filter_should_return_only_views() throws SQLException {
try (Statement stmt = connection.createStatement()) {
stmt.execute("CREATE TABLE my_table (id INTEGER PRIMARY KEY)");
stmt.execute("CREATE VIEW my_view AS SELECT * FROM my_table");
}
ResultSet rs = metaData.getTables(null, null, null, new String[] {"VIEW"});
assertNotNull(rs);
assertTrue(rs.next());
assertEquals("my_view", rs.getString("TABLE_NAME"));
assertEquals("VIEW", rs.getString("TABLE_TYPE"));
assertFalse(rs.next());
rs.close();
}
@Test
@Disabled("CREATE VIEW not supported yet")
void getTables_with_pattern_and_type_filter_should_work_together() throws SQLException {
try (Statement stmt = connection.createStatement()) {
stmt.execute("CREATE TABLE alpha (id INTEGER)");
stmt.execute("CREATE TABLE beta (id INTEGER)");
stmt.execute("CREATE VIEW alpha_view AS SELECT * FROM alpha");
}
ResultSet rs = metaData.getTables(null, null, "alpha%", new String[] {"VIEW"});
assertNotNull(rs);
assertTrue(rs.next());
assertEquals("alpha_view", rs.getString("TABLE_NAME"));
assertEquals("VIEW", rs.getString("TABLE_TYPE"));
assertFalse(rs.next());
rs.close();
}
}