diff --git a/bindings/java/src/main/java/tech/turso/core/LimboResultSet.java b/bindings/java/src/main/java/tech/turso/core/LimboResultSet.java index 4e6877fc7..0f38e72c0 100644 --- a/bindings/java/src/main/java/tech/turso/core/LimboResultSet.java +++ b/bindings/java/src/main/java/tech/turso/core/LimboResultSet.java @@ -119,6 +119,18 @@ public final class LimboResultSet { this.open = false; } + @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"); + } + // Note that columnIndex starts from 1 @Nullable public Object get(int columnIndex) throws SQLException { diff --git a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4DatabaseMetaData.java b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4DatabaseMetaData.java index fd159d2bd..ca233e234 100644 --- a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4DatabaseMetaData.java +++ b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4DatabaseMetaData.java @@ -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 diff --git a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSet.java b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSet.java index 9fbead092..4efc8a61e 100644 --- a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSet.java +++ b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSet.java @@ -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 diff --git a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSetMetadata.java b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSetMetadata.java index 73f6a4be7..a1982a713 100644 --- a/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSetMetadata.java +++ b/bindings/java/src/main/java/tech/turso/jdbc4/JDBC4ResultSetMetadata.java @@ -2,150 +2,152 @@ package tech.turso.jdbc4; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import tech.turso.annotations.SkipNullableCheck; public class JDBC4ResultSetMetadata implements ResultSetMetaData { - private final JDBC4ResultSet resultSet; + private final JDBC4ResultSet resultSet; - public JDBC4ResultSetMetadata(JDBC4ResultSet resultSet) { - this.resultSet = resultSet; - } + public JDBC4ResultSetMetadata(JDBC4ResultSet resultSet) { + this.resultSet = resultSet; + } - @Override - public int getColumnCount() throws SQLException { - // TODO - return 0; - } + @Override + public int getColumnCount() throws SQLException { + // TODO + return 0; + } - @Override - public boolean isAutoIncrement(int column) throws SQLException { - // TODO - return false; - } + @Override + public boolean isAutoIncrement(int column) throws SQLException { + // TODO + return false; + } - @Override - public boolean isCaseSensitive(int column) throws SQLException { - // TODO - return false; - } + @Override + public boolean isCaseSensitive(int column) throws SQLException { + // TODO + return false; + } - @Override - public boolean isSearchable(int column) throws SQLException { - // TODO - return false; - } + @Override + public boolean isSearchable(int column) throws SQLException { + // TODO + return false; + } - @Override - public boolean isCurrency(int column) throws SQLException { - // TODO - return false; - } + @Override + public boolean isCurrency(int column) throws SQLException { + // TODO + return false; + } - @Override - public int isNullable(int column) throws SQLException { - // TODO - return 0; - } + @Override + public int isNullable(int column) throws SQLException { + // TODO + return 0; + } - @Override - public boolean isSigned(int column) throws SQLException { - // TODO - return false; - } + @Override + public boolean isSigned(int column) throws SQLException { + // TODO + return false; + } - @Override - public int getColumnDisplaySize(int column) throws SQLException { - // TODO - return 0; - } + @Override + public int getColumnDisplaySize(int column) throws SQLException { + // TODO + return 0; + } - @Override - public String getColumnLabel(int column) throws SQLException { - // TODO - return ""; - } + @Override + public String getColumnLabel(int column) throws SQLException { + // TODO + return ""; + } - @Override - public String getColumnName(int column) throws SQLException { - // TODO - return ""; - } + @Override + public String getColumnName(int column) throws SQLException { + // TODO + return ""; + } - @Override - public String getSchemaName(int column) throws SQLException { - // TODO - return ""; - } + @Override + public String getSchemaName(int column) throws SQLException { + // TODO + return ""; + } - @Override - public int getPrecision(int column) throws SQLException { - // TODO - return 0; - } + @Override + public int getPrecision(int column) throws SQLException { + // TODO + return 0; + } - @Override - public int getScale(int column) throws SQLException { - // TODO - return 0; - } + @Override + public int getScale(int column) throws SQLException { + // TODO + return 0; + } - @Override - public String getTableName(int column) throws SQLException { - // TODO - return ""; - } + @Override + public String getTableName(int column) throws SQLException { + // TODO + return ""; + } - @Override - public String getCatalogName(int column) throws SQLException { - // TODO - return ""; - } + @Override + public String getCatalogName(int column) throws SQLException { + // TODO + return ""; + } - @Override - public int getColumnType(int column) throws SQLException { - // TODO - return 0; - } + @Override + public int getColumnType(int column) throws SQLException { + // TODO + return 0; + } - @Override - public String getColumnTypeName(int column) throws SQLException { - // TODO - return ""; - } + @Override + public String getColumnTypeName(int column) throws SQLException { + // TODO + return ""; + } - @Override - public boolean isReadOnly(int column) throws SQLException { - // TODO - return false; - } + @Override + public boolean isReadOnly(int column) throws SQLException { + // TODO + return false; + } - @Override - public boolean isWritable(int column) throws SQLException { - // TODO - return false; - } + @Override + public boolean isWritable(int column) throws SQLException { + // TODO + return false; + } - @Override - public boolean isDefinitelyWritable(int column) throws SQLException { - // TODO - return false; - } + @Override + public boolean isDefinitelyWritable(int column) throws SQLException { + // TODO + return false; + } - @Override - public String getColumnClassName(int column) throws SQLException { - // TODO - return ""; - } + @Override + public String getColumnClassName(int column) throws SQLException { + // TODO + return ""; + } - @Override - public T unwrap(Class iface) throws SQLException { - // TODO - return null; - } + @Override + @SkipNullableCheck + public T unwrap(Class iface) throws SQLException { + // TODO + return null; + } - @Override - public boolean isWrapperFor(Class iface) throws SQLException { - // TODO - return false; - } + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + // TODO + return false; + } } diff --git a/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4DatabaseMetaDataTest.java b/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4DatabaseMetaDataTest.java index 149516039..41c797580 100644 --- a/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4DatabaseMetaDataTest.java +++ b/bindings/java/src/test/java/tech/turso/jdbc4/JDBC4DatabaseMetaDataTest.java @@ -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(); + } }