From ec0ad24206f319220f90faac734d2b2cbd8ee76d Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Mon, 13 Jan 2025 20:03:01 +0100 Subject: [PATCH 1/7] Upgrade dependencies --- settings.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/settings.gradle b/settings.gradle index d32e037..9b5f2c8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,13 +10,13 @@ dependencyResolutionManagement { } versionCatalogs { libs { - version('mockito', '5.14.2') + version('mockito', '5.15.2') version('junitJupiter', '5.11.2') - library('assertj', 'org.assertj:assertj-core:3.27.0') + library('assertj', 'org.assertj:assertj-core:3.27.2') library('h2', 'com.h2database:h2:2.3.232') library('junitPioneer', 'org.junit-pioneer:junit-pioneer:2.2.0') - library('equalsverifier', 'nl.jqno.equalsverifier:equalsverifier:3.18') + library('equalsverifier', 'nl.jqno.equalsverifier:equalsverifier:3.18.1') library('tostringverifier', 'com.jparams:to-string-verifier:1.4.8') library('hamcrest', 'org.hamcrest:hamcrest:3.0') library('hamcrestResultSetMatcher', 'com.exasol:hamcrest-resultset-matcher:1.6.3') From b462548ae0ceb83b838fdd743e46de3bfc56a8e9 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Mon, 13 Jan 2025 21:15:17 +0100 Subject: [PATCH 2/7] Add support for database metadata --- CHANGELOG.md | 1 + .../itsallcode/jdbc/ConnectionWrapper.java | 12 + .../java/org/itsallcode/jdbc/DbMetaData.java | 256 ++++++++++++++++++ .../org/itsallcode/jdbc/SimpleConnection.java | 10 + .../jdbc/resultset/SimpleResultSet.java | 10 + .../org/itsallcode/jdbc/DbMetaDataITest.java | 100 +++++++ .../itsallcode/jdbc/SimpleConnectionTest.java | 3 +- 7 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/itsallcode/jdbc/DbMetaData.java create mode 100644 src/test/java/org/itsallcode/jdbc/DbMetaDataITest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 696f1c0..2138914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [PR #45](https://github.com/itsallcode/simple-jdbc/pull/45): Rename `executeStatement()` to `executeUpdate()` and return row count (**Breaking change**) - [PR #46](https://github.com/itsallcode/simple-jdbc/pull/46): Close `Statement` / `PreparedStatement` when closing the result set. - [PR #47](https://github.com/itsallcode/simple-jdbc/pull/47): Rename `BatchInsert` to `PreparedStatementBatch`, allow specifying a custom SQL statement (**Breaking change**) +- [PR #48](https://github.com/itsallcode/simple-jdbc/pull/48): Add support for database metadata ## [0.9.0] - 2024-12-23 diff --git a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java index 342cbf9..fd685cb 100644 --- a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java +++ b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java @@ -109,6 +109,18 @@ private Statement createStatement() { } } + DbMetaData getMetaData() { + return new DbMetaData(this.context, getMetaDataInternal()); + } + + private DatabaseMetaData getMetaDataInternal() { + try { + return connection.getMetaData(); + } catch (final SQLException e) { + throw new UncheckedSQLException("Failed to get metadata ", e); + } + } + void setAutoCommit(final boolean autoCommit) { try { connection.setAutoCommit(autoCommit); diff --git a/src/main/java/org/itsallcode/jdbc/DbMetaData.java b/src/main/java/org/itsallcode/jdbc/DbMetaData.java new file mode 100644 index 0000000..13c376f --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/DbMetaData.java @@ -0,0 +1,256 @@ +package org.itsallcode.jdbc; + +import java.sql.*; + +import org.itsallcode.jdbc.resultset.*; + +/** + * A simple wrapper for {@link DatabaseMetaData}. + */ +public class DbMetaData { + + private final DatabaseMetaData metaData; + private final Context context; + + DbMetaData(final Context context, final DatabaseMetaData metaData) { + this.context = context; + this.metaData = metaData; + } + + /** + * Retrieves a description of the tables available in the given catalog. Only + * table descriptions matching the catalog, schema, table name and type criteria + * are returned. They are ordered by {@code TABLE_TYPE}, {@code TABLE_CAT}, + * {@code TABLE_SCHEM} and {@code TABLE_NAME}. + * + * @param catalog a catalog name; must match the catalog name as it is + * stored in the database; "" retrieves those without a + * catalog; {@code null} means that the catalog name + * should not be used to narrow the search + * @param schemaPattern a schema name pattern; must match the schema name as + * it is stored in the database; "" retrieves those + * without a schema; {@code null} means that the schema + * name should not be used to narrow the search + * @param tableNamePattern a table name pattern; must match the table name as it + * is stored in the database + * @param types a list of table types, which must be from the list of + * table types returned from + * {@link DatabaseMetaData#getTableTypes()},to include; + * {@code null} returns all types + * @return table descriptions + * @see DatabaseMetaData#getTables(String, String, String, String[]) + */ + public SimpleResultSet getTables(final String catalog, final String schemaPattern, + final String tableNamePattern, final String[] types) { + return createResultSet(getTablesInternal(catalog, schemaPattern, tableNamePattern, types), + TableMetaData::create); + } + + /** + * Description of a table. + *
    + *
  1. TABLE_CAT String {@code =>} table catalog (may be {@code null}) + *
  2. TABLE_SCHEM String {@code =>} table schema (may be {@code null}) + *
  3. TABLE_NAME String {@code =>} table name + *
  4. TABLE_TYPE String {@code =>} table type. Typical types are + * "TABLE", "VIEW", "SYSTEM TABLE", "GLOBAL TEMPORARY", "LOCAL TEMPORARY", + * "ALIAS", "SYNONYM". + *
  5. REMARKS String {@code =>} explanatory comment on the table (may be + * {@code null}) + *
  6. TYPE_CAT String {@code =>} the types catalog (may be {@code null}) + *
  7. TYPE_SCHEM String {@code =>} the types schema (may be + * {@code null}) + *
  8. TYPE_NAME String {@code =>} type name (may be {@code null}) + *
  9. SELF_REFERENCING_COL_NAME String {@code =>} name of the designated + * "identifier" column of a typed table (may be {@code null}) + *
  10. REF_GENERATION String {@code =>} specifies how values in + * SELF_REFERENCING_COL_NAME are created. Values are "SYSTEM", "USER", + * "DERIVED". (may be {@code null}) + *
+ */ + public record TableMetaData(String tableCatalog, String tableSchema, String tableName, String tableType, + String remarks, String typeCatalog, String typeSchema, String typeName, String selfReferencingColumnName, + String refGeneration) { + private static TableMetaData create(final ResultSet rs, final int rowNum) throws SQLException { + return new TableMetaData( + rs.getString("TABLE_CAT"), rs.getString("TABLE_SCHEM"), + rs.getString("TABLE_NAME"), + rs.getString("TABLE_TYPE"), + rs.getString("REMARKS"), + rs.getString("TYPE_CAT"), rs.getString("TYPE_SCHEM"), + rs.getString("TYPE_NAME"), + rs.getString("SELF_REFERENCING_COL_NAME"), + rs.getString("REF_GENERATION")); + } + } + + private ResultSet getTablesInternal(final String catalog, final String schemaPattern, final String tableNamePattern, + final String[] types) { + try { + return metaData.getTables(catalog, schemaPattern, tableNamePattern, types); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error getting tables", e); + } + } + + /** + * Retrieves a description of table columns available in the specified catalog. + *

+ * Only column descriptions matching the catalog, schema, table and column name + * criteria are returned. They are ordered by + * {@code TABLE_CAT},{@code TABLE_SCHEM}, {@code TABLE_NAME}, and + * {@code ORDINAL_POSITION}. + * + * @param catalog a catalog name; must match the catalog name as it is + * stored in the database; "" retrieves those without a + * catalog; {@code null} means that the catalog name + * should not be used to narrow the search + * @param schemaPattern a schema name pattern; must match the schema name as + * it is stored in the database; "" retrieves those + * without a schema; {@code null} means that the schema + * name should not be used to narrow the search + * @param tableNamePattern a table name pattern; must match the table name as + * it is stored in the database + * @param columnNamePattern a column name pattern; must match the column name as + * it is stored in the database + * @return column descriptions + * @see DatabaseMetaData#getColumns(String, String, String, String) + */ + public SimpleResultSet getColumns(final String catalog, final String schemaPattern, + final String tableNamePattern, final String columnNamePattern) { + return createResultSet(getColumnsInternal(catalog, schemaPattern, tableNamePattern, columnNamePattern), + ColumnMetaData::create); + } + + private ResultSet getColumnsInternal(final String catalog, final String schemaPattern, + final String tableNamePattern, final String columnNamePattern) { + try { + return metaData.getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error getting tables", e); + } + } + + private SimpleResultSet createResultSet(final ResultSet resultSet, final RowMapper rowMapper) { + return new SimpleResultSet<>(context, resultSet, ContextRowMapper.create(rowMapper), () -> { + }); + } + + /** + *

    + *
  1. TABLE_CAT String {@code =>} table catalog (may be {@code null}) + *
  2. TABLE_SCHEM String {@code =>} table schema (may be {@code null}) + *
  3. TABLE_NAME String {@code =>} table name + *
  4. COLUMN_NAME String {@code =>} column name + *
  5. DATA_TYPE int {@code =>} SQL type from java.sql.Types + *
  6. TYPE_NAME String {@code =>} Data source dependent type name, for a + * UDT the type name is fully qualified + *
  7. COLUMN_SIZE int {@code =>} column size. + *
  8. BUFFER_LENGTH is not used. + *
  9. DECIMAL_DIGITS int {@code =>} the number of fractional digits. + * Null is returned for data types where DECIMAL_DIGITS is not applicable. + *
  10. NUM_PREC_RADIX int {@code =>} Radix (typically either 10 or 2) + *
  11. NULLABLE int {@code =>} is NULL allowed. + *
      + *
    • columnNoNulls - might not allow {@code NULL} values + *
    • columnNullable - definitely allows {@code NULL} values + *
    • columnNullableUnknown - nullability unknown + *
    + *
  12. REMARKS String {@code =>} comment describing column (may be + * {@code null}) + *
  13. COLUMN_DEF String {@code =>} default value for the column, which + * should be interpreted as a string when the value is enclosed in single quotes + * (may be {@code null}) + *
  14. SQL_DATA_TYPE int {@code =>} unused + *
  15. SQL_DATETIME_SUB int {@code =>} unused + *
  16. CHAR_OCTET_LENGTH int {@code =>} for char types the maximum number + * of bytes in the column + *
  17. ORDINAL_POSITION int {@code =>} index of column in table (starting + * at 1) + *
  18. IS_NULLABLE String {@code =>} ISO rules are used to determine the + * nullability for a column. + *
      + *
    • YES --- if the column can include NULLs + *
    • NO --- if the column cannot include NULLs + *
    • empty string --- if the nullability for the column is unknown + *
    + *
  19. SCOPE_CATALOG String {@code =>} catalog of table that is the scope + * of a reference attribute ({@code null} if DATA_TYPE isn't REF) + *
  20. SCOPE_SCHEMA String {@code =>} schema of table that is the scope + * of a reference attribute ({@code null} if the DATA_TYPE isn't REF) + *
  21. SCOPE_TABLE String {@code =>} table name that this the scope of a + * reference attribute ({@code null} if the DATA_TYPE isn't REF) + *
  22. SOURCE_DATA_TYPE short {@code =>} source type of a distinct type + * or user-generated Ref type, SQL type from java.sql.Types ({@code null} if + * DATA_TYPE isn't DISTINCT or user-generated REF) + *
  23. IS_AUTOINCREMENT String {@code =>} Indicates whether this column + * is auto incremented + *
      + *
    • YES --- if the column is auto incremented + *
    • NO --- if the column is not auto incremented + *
    • empty string --- if it cannot be determined whether the column is auto + * incremented + *
    + *
  24. IS_GENERATEDCOLUMN String {@code =>} Indicates whether this is a + * generated column + *
      + *
    • YES --- if this a generated column + *
    • NO --- if this not a generated column + *
    • empty string --- if it cannot be determined whether this is a generated + * column + *
    + *
+ */ + public record ColumnMetaData( + String tableCatalog, + String tableSchema, + String tableName, + String columnName, + int dataType, + String typeName, + int columnSize, + int decimalDigits, + int numPrecisionRadix, + String nullable, + String remarks, + String columnDef, + int sqlDataType, + int sqlDatetimeSub, + int charOctetLength, + int ordinalPosition, + String isNullable, + String scopeCatalog, + String scopeSchema, + String scopeTable, + short sourceDataType, + String isAutoIncrement, + String isGeneratedColumn) { + + private static ColumnMetaData create(final ResultSet rs, final int rowNum) throws SQLException { + return new ColumnMetaData( + rs.getString("TABLE_CAT"), + rs.getString("TABLE_SCHEM"), + rs.getString("TABLE_NAME"), + rs.getString("COLUMN_NAME"), + rs.getInt("DATA_TYPE"), + rs.getString("TYPE_NAME"), + rs.getInt("COLUMN_SIZE"), + rs.getInt("DECIMAL_DIGITS"), + rs.getInt("NUM_PREC_RADIX"), + rs.getString("NULLABLE"), + rs.getString("REMARKS"), + rs.getString("COLUMN_DEF"), + rs.getInt("SQL_DATA_TYPE"), + rs.getInt("SQL_DATETIME_SUB"), + rs.getInt("CHAR_OCTET_LENGTH"), + rs.getInt("ORDINAL_POSITION"), + rs.getString("IS_NULLABLE"), + rs.getString("SCOPE_CATALOG"), + rs.getString("SCOPE_SCHEMA"), + rs.getString("SCOPE_TABLE"), + rs.getShort("SOURCE_DATA_TYPE"), + rs.getString("IS_AUTOINCREMENT"), + rs.getString("IS_GENERATEDCOLUMN")); + } + } +} diff --git a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java index cead7f9..6c68951 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java @@ -117,6 +117,16 @@ public RowPreparedStatementBatchBuilder preparedStatementBatch(final Clas return connection.rowPreparedStatementBatch(); } + /** + * Get database metadata. + * + * @return metadata + */ + public DbMetaData getMetaData() { + checkOperationAllowed(); + return connection.getMetaData(); + } + public Connection getOriginalConnection() { checkOperationAllowed(); return connection.getOriginalConnection(); diff --git a/src/main/java/org/itsallcode/jdbc/resultset/SimpleResultSet.java b/src/main/java/org/itsallcode/jdbc/resultset/SimpleResultSet.java index a4131c2..941d3c6 100644 --- a/src/main/java/org/itsallcode/jdbc/resultset/SimpleResultSet.java +++ b/src/main/java/org/itsallcode/jdbc/resultset/SimpleResultSet.java @@ -8,6 +8,7 @@ import org.itsallcode.jdbc.Context; import org.itsallcode.jdbc.UncheckedSQLException; +import org.itsallcode.jdbc.resultset.generic.SimpleMetaData; /** * This class wraps a {@link ResultSet} and allows easy iteration via @@ -39,6 +40,15 @@ public SimpleResultSet(final Context context, final ResultSet resultSet, final C this.statement = statement; } + /** + * Get result set metadata. + * + * @return metadata + */ + public SimpleMetaData getMetaData() { + return SimpleMetaData.create(this.resultSet); + } + /** * Get in {@link Iterator} of all rows. * diff --git a/src/test/java/org/itsallcode/jdbc/DbMetaDataITest.java b/src/test/java/org/itsallcode/jdbc/DbMetaDataITest.java new file mode 100644 index 0000000..e4f2fda --- /dev/null +++ b/src/test/java/org/itsallcode/jdbc/DbMetaDataITest.java @@ -0,0 +1,100 @@ +package org.itsallcode.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.itsallcode.jdbc.DbMetaData.ColumnMetaData; +import org.itsallcode.jdbc.DbMetaData.TableMetaData; +import org.itsallcode.jdbc.resultset.SimpleResultSet; +import org.junit.jupiter.api.Test; + +class DbMetaDataITest { + + @Test + void getTablesNoResult() { + try (final SimpleConnection connection = H2TestFixture.createMemConnection()) { + final List tables = connection.getMetaData().getTables("unknown", null, null, null).toList(); + assertThat(tables).isEmpty(); + } + } + + @Test + void getTables() { + try (final SimpleConnection connection = H2TestFixture.createMemConnection()) { + final List tables = connection.getMetaData().getTables(null, null, null, null).toList(); + assertThat(tables) + .hasSize(35) + .first() + .isEqualTo(new TableMetaData("UNNAMED", "INFORMATION_SCHEMA", "CONSTANTS", "BASE TABLE", + null, null, null, null, null, null)); + } + } + + @Test + void getTablesMetaData() { + try (final SimpleConnection connection = H2TestFixture.createMemConnection(); + final SimpleResultSet result = connection.getMetaData().getTables(null, null, null, + null)) { + assertThat(result.getMetaData().columns()) + .extracting(org.itsallcode.jdbc.resultset.generic.ColumnMetaData::name).containsExactly("TABLE_CAT", + "TABLE_SCHEM", "TABLE_NAME", "TABLE_TYPE", "REMARKS", "TYPE_CAT", "TYPE_SCHEM", "TYPE_NAME", + "SELF_REFERENCING_COL_NAME", "REF_GENERATION"); + } + } + + @Test + void getColumnsNoResult() { + try (final SimpleConnection connection = H2TestFixture.createMemConnection()) { + final List columns = connection.getMetaData().getColumns("unknown", null, null, null) + .toList(); + assertThat(columns).isEmpty(); + } + } + + @Test + void getColumns() { + try (final SimpleConnection connection = H2TestFixture.createMemConnection()) { + final List columns = connection.getMetaData().getColumns(null, null, null, null).toList(); + assertThat(columns) + .hasSize(451) + .first() + .isEqualTo(new ColumnMetaData("UNNAMED", "INFORMATION_SCHEMA", "CHECK_CONSTRAINTS", + "CONSTRAINT_CATALOG", 12, "CHARACTER VARYING", 1000000000, 0, + 0, "1", null, null, 0, 0, 1000000000, 1, "YES", null, null, null, (short) 0, "NO", "NO")); + } + } + + @Test + void getColumnsFilterByTable() { + try (final SimpleConnection connection = H2TestFixture.createMemConnection()) { + final List columns = connection.getMetaData() + .getColumns(null, null, "CHECK_CONSTRAINTS", null).toList(); + assertThat(columns).hasSize(4); + } + } + + @Test + void getColumnsFilterByColumn() { + try (final SimpleConnection connection = H2TestFixture.createMemConnection()) { + final List columns = connection.getMetaData() + .getColumns(null, null, "CHECK_CONSTRAINTS", "CONSTRAINT_CATALOG").toList(); + assertThat(columns).hasSize(1); + } + } + + @Test + void getColumnsMetaData() { + try (final SimpleConnection connection = H2TestFixture.createMemConnection(); + final SimpleResultSet result = connection.getMetaData().getColumns(null, null, null, + null)) { + assertThat(result.getMetaData().columns()) + .extracting(org.itsallcode.jdbc.resultset.generic.ColumnMetaData::name) + .containsExactly("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"); + } + } +} diff --git a/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java b/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java index 10435c5..7f61fc7 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleConnectionTest.java @@ -74,7 +74,8 @@ static Stream operations() { operation(con -> con.getOriginalConnection()), operation(con -> con.statementBatch()), operation(con -> con.preparedStatementBatch()), - operation(con -> con.preparedStatementBatch(null))); + operation(con -> con.preparedStatementBatch(null)), + operation(con -> con.getMetaData())); } static Arguments operation(final Consumer operation) { From d418cbd6cf1d15125d93e4a95495d3d56d450d18 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Tue, 14 Jan 2025 20:16:43 +0100 Subject: [PATCH 3/7] Fix javadoc --- .../java/org/itsallcode/jdbc/DbMetaData.java | 166 +++++++++--------- 1 file changed, 80 insertions(+), 86 deletions(-) diff --git a/src/main/java/org/itsallcode/jdbc/DbMetaData.java b/src/main/java/org/itsallcode/jdbc/DbMetaData.java index 13c376f..8727409 100644 --- a/src/main/java/org/itsallcode/jdbc/DbMetaData.java +++ b/src/main/java/org/itsallcode/jdbc/DbMetaData.java @@ -48,25 +48,25 @@ public SimpleResultSet getTables(final String catalog, final Stri /** * Description of a table. - *
    - *
  1. TABLE_CAT String {@code =>} table catalog (may be {@code null}) - *
  2. TABLE_SCHEM String {@code =>} table schema (may be {@code null}) - *
  3. TABLE_NAME String {@code =>} table name - *
  4. TABLE_TYPE String {@code =>} table type. Typical types are - * "TABLE", "VIEW", "SYSTEM TABLE", "GLOBAL TEMPORARY", "LOCAL TEMPORARY", - * "ALIAS", "SYNONYM". - *
  5. REMARKS String {@code =>} explanatory comment on the table (may be - * {@code null}) - *
  6. TYPE_CAT String {@code =>} the types catalog (may be {@code null}) - *
  7. TYPE_SCHEM String {@code =>} the types schema (may be - * {@code null}) - *
  8. TYPE_NAME String {@code =>} type name (may be {@code null}) - *
  9. SELF_REFERENCING_COL_NAME String {@code =>} name of the designated - * "identifier" column of a typed table (may be {@code null}) - *
  10. REF_GENERATION String {@code =>} specifies how values in - * SELF_REFERENCING_COL_NAME are created. Values are "SYSTEM", "USER", - * "DERIVED". (may be {@code null}) - *
+ * + * @param tableCatalog table catalog (may be {@code null}) + * @param tableSchema table schema (may be {@code null}) + * @param tableName table name + * @param tableType table type. Typical types are "TABLE", + * "VIEW", "SYSTEM TABLE", "GLOBAL TEMPORARY", + * "LOCAL TEMPORARY", "ALIAS", "SYNONYM". + * @param remarks explanatory comment on the table (may be + * {@code null}) + * @param typeCatalog the types catalog (may be {@code null}) + * @param typeSchema the types schema (may be {@code null}) + * @param typeName type name (may be {@code null}) + * @param selfReferencingColumnName name of the designated "identifier" column + * of a typed table (may be {@code null}) + * @param refGeneration specifies how values in + * SELF_REFERENCING_COL_NAME are created. + * Values are "SYSTEM", "USER", "DERIVED". (may + * be {@code null}) + * @see DatabaseMetaData#getTables(String, String, String, String[]) */ public record TableMetaData(String tableCatalog, String tableSchema, String tableName, String tableType, String remarks, String typeCatalog, String typeSchema, String typeName, String selfReferencingColumnName, @@ -137,69 +137,67 @@ private SimpleResultSet createResultSet(final ResultSet resultSet, final } /** - *
    - *
  1. TABLE_CAT String {@code =>} table catalog (may be {@code null}) - *
  2. TABLE_SCHEM String {@code =>} table schema (may be {@code null}) - *
  3. TABLE_NAME String {@code =>} table name - *
  4. COLUMN_NAME String {@code =>} column name - *
  5. DATA_TYPE int {@code =>} SQL type from java.sql.Types - *
  6. TYPE_NAME String {@code =>} Data source dependent type name, for a - * UDT the type name is fully qualified - *
  7. COLUMN_SIZE int {@code =>} column size. - *
  8. BUFFER_LENGTH is not used. - *
  9. DECIMAL_DIGITS int {@code =>} the number of fractional digits. - * Null is returned for data types where DECIMAL_DIGITS is not applicable. - *
  10. NUM_PREC_RADIX int {@code =>} Radix (typically either 10 or 2) - *
  11. NULLABLE int {@code =>} is NULL allowed. - *
      - *
    • columnNoNulls - might not allow {@code NULL} values - *
    • columnNullable - definitely allows {@code NULL} values - *
    • columnNullableUnknown - nullability unknown - *
    - *
  12. REMARKS String {@code =>} comment describing column (may be - * {@code null}) - *
  13. COLUMN_DEF String {@code =>} default value for the column, which - * should be interpreted as a string when the value is enclosed in single quotes - * (may be {@code null}) - *
  14. SQL_DATA_TYPE int {@code =>} unused - *
  15. SQL_DATETIME_SUB int {@code =>} unused - *
  16. CHAR_OCTET_LENGTH int {@code =>} for char types the maximum number - * of bytes in the column - *
  17. ORDINAL_POSITION int {@code =>} index of column in table (starting - * at 1) - *
  18. IS_NULLABLE String {@code =>} ISO rules are used to determine the - * nullability for a column. - *
      - *
    • YES --- if the column can include NULLs - *
    • NO --- if the column cannot include NULLs - *
    • empty string --- if the nullability for the column is unknown - *
    - *
  19. SCOPE_CATALOG String {@code =>} catalog of table that is the scope - * of a reference attribute ({@code null} if DATA_TYPE isn't REF) - *
  20. SCOPE_SCHEMA String {@code =>} schema of table that is the scope - * of a reference attribute ({@code null} if the DATA_TYPE isn't REF) - *
  21. SCOPE_TABLE String {@code =>} table name that this the scope of a - * reference attribute ({@code null} if the DATA_TYPE isn't REF) - *
  22. SOURCE_DATA_TYPE short {@code =>} source type of a distinct type - * or user-generated Ref type, SQL type from java.sql.Types ({@code null} if - * DATA_TYPE isn't DISTINCT or user-generated REF) - *
  23. IS_AUTOINCREMENT String {@code =>} Indicates whether this column - * is auto incremented - *
      - *
    • YES --- if the column is auto incremented - *
    • NO --- if the column is not auto incremented - *
    • empty string --- if it cannot be determined whether the column is auto - * incremented - *
    - *
  24. IS_GENERATEDCOLUMN String {@code =>} Indicates whether this is a - * generated column - *
      - *
    • YES --- if this a generated column - *
    • NO --- if this not a generated column - *
    • empty string --- if it cannot be determined whether this is a generated - * column - *
    - *
+ * Description of a column. + * + * @param tableCatalog table catalog (may be {@code null}) + * @param tableSchema table schema (may be {@code null}) + * @param tableName table name + * @param columnName column name + * @param dataType SQL type from java.sql.Types + * @param typeName Data source dependent type name, for a UDT the type + * name is fully qualified + * @param columnSize column size. + * @param decimalDigits the number of fractional digits. Null is returned + * for data types where DECIMAL_DIGITS is not + * applicable. + * @param numPrecisionRadix Radix (typically either 10 or 2) + * @param nullable is NULL allowed. + *
    + *
  • columnNoNulls - might not allow {@code NULL} + * values + *
  • columnNullable - definitely allows {@code NULL} + * values + *
  • columnNullableUnknown - nullability unknown + *
+ * @param remarks comment describing column (may be {@code null}) + * @param columnDef default value for the column, which should be + * interpreted as a string when the value is enclosed + * in single quotes (may be {@code null}) + * @param charOctetLength for char types the maximum number of bytes in the + * column + * @param ordinalPosition index of column in table (starting at 1) + * @param isNullable ISO rules are used to determine the nullability for + * a column. + *
    + *
  • YES --- if the column can include NULLs + *
  • NO --- if the column cannot include NULLs + *
  • empty string --- if the nullability for the + * column is unknown + *
+ * @param scopeCatalog catalog of table that is the scope of a reference + * attribute ({@code null} if DATA_TYPE isn't REF) + * @param scopeSchema schema of table that is the scope of a reference + * attribute ({@code null} if the DATA_TYPE isn't REF) + * @param scopeTable table name that this the scope of a reference + * attribute ({@code null} if the DATA_TYPE isn't REF) + * @param sourceDataType source type of a distinct type or user-generated Ref + * type, SQL type from java.sql.Types ({@code null} if + * DATA_TYPE isn't DISTINCT or user-generated REF) + * @param isAutoIncrement Indicates whether this column is auto incremented + *
    + *
  • YES --- if the column is auto incremented + *
  • NO --- if the column is not auto incremented + *
  • empty string --- if it cannot be determined + * whether the column is auto incremented + *
+ * @param isGeneratedColumn Indicates whether this is a generated column + *
    + *
  • YES --- if this a generated column + *
  • NO --- if this not a generated column + *
  • empty string --- if it cannot be determined + * whether this is a generated column + *
+ * @see DatabaseMetaData#getColumns(String, String, String, String) */ public record ColumnMetaData( String tableCatalog, @@ -214,8 +212,6 @@ public record ColumnMetaData( String nullable, String remarks, String columnDef, - int sqlDataType, - int sqlDatetimeSub, int charOctetLength, int ordinalPosition, String isNullable, @@ -240,8 +236,6 @@ private static ColumnMetaData create(final ResultSet rs, final int rowNum) throw rs.getString("NULLABLE"), rs.getString("REMARKS"), rs.getString("COLUMN_DEF"), - rs.getInt("SQL_DATA_TYPE"), - rs.getInt("SQL_DATETIME_SUB"), rs.getInt("CHAR_OCTET_LENGTH"), rs.getInt("ORDINAL_POSITION"), rs.getString("IS_NULLABLE"), From 9b01f59e33bf6f4a465f0c7f9bdbad2ef29972d9 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sat, 18 Jan 2025 17:18:38 +0100 Subject: [PATCH 4/7] Refactor metadata --- .../org/itsallcode/jdbc/ExasolTypeTest.java | 6 +- src/main/java/module-info.java | 1 + .../itsallcode/jdbc/ConnectionWrapper.java | 1 + .../java/org/itsallcode/jdbc/DbMetaData.java | 250 ------------------ .../org/itsallcode/jdbc/SimpleConnection.java | 1 + .../jdbc/metadata/ColumnMetaData.java | 195 ++++++++++++++ .../itsallcode/jdbc/metadata/DbMetaData.java | 108 ++++++++ .../jdbc/metadata/TableMetaData.java | 43 +++ .../org/itsallcode/jdbc/DbMetaDataITest.java | 12 +- 9 files changed, 360 insertions(+), 257 deletions(-) delete mode 100644 src/main/java/org/itsallcode/jdbc/DbMetaData.java create mode 100644 src/main/java/org/itsallcode/jdbc/metadata/ColumnMetaData.java create mode 100644 src/main/java/org/itsallcode/jdbc/metadata/DbMetaData.java create mode 100644 src/main/java/org/itsallcode/jdbc/metadata/TableMetaData.java diff --git a/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java b/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java index dee131a..4df6bef 100644 --- a/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java +++ b/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java @@ -13,7 +13,8 @@ import java.util.stream.Stream; import org.itsallcode.jdbc.resultset.SimpleResultSet; -import org.itsallcode.jdbc.resultset.generic.*; +import org.itsallcode.jdbc.resultset.generic.ColumnValue; +import org.itsallcode.jdbc.resultset.generic.Row; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -155,8 +156,7 @@ void countStarResultType() { final SimpleResultSet result = connection .query("select count(*) from (select 1 from dual)")) { final Row row = result.toList().get(0); - final ColumnMetaData columnMetaData = row.columns().get(0); - assertThat(columnMetaData.type().jdbcType()).isEqualTo(JDBCType.BIGINT); + assertThat(row.columns().get(0).type().jdbcType()).isEqualTo(JDBCType.BIGINT); } } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 90fb6ee..3ed42ce 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -74,6 +74,7 @@ exports org.itsallcode.jdbc.resultset; exports org.itsallcode.jdbc.resultset.generic; exports org.itsallcode.jdbc.dialect; + exports org.itsallcode.jdbc.metadata; requires java.logging; requires transitive java.sql; diff --git a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java index fd685cb..35daab3 100644 --- a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java +++ b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java @@ -8,6 +8,7 @@ import org.itsallcode.jdbc.batch.*; import org.itsallcode.jdbc.dialect.DbDialect; +import org.itsallcode.jdbc.metadata.DbMetaData; import org.itsallcode.jdbc.resultset.*; import org.itsallcode.jdbc.resultset.generic.Row; import org.itsallcode.jdbc.statement.ConvertingPreparedStatement; diff --git a/src/main/java/org/itsallcode/jdbc/DbMetaData.java b/src/main/java/org/itsallcode/jdbc/DbMetaData.java deleted file mode 100644 index 8727409..0000000 --- a/src/main/java/org/itsallcode/jdbc/DbMetaData.java +++ /dev/null @@ -1,250 +0,0 @@ -package org.itsallcode.jdbc; - -import java.sql.*; - -import org.itsallcode.jdbc.resultset.*; - -/** - * A simple wrapper for {@link DatabaseMetaData}. - */ -public class DbMetaData { - - private final DatabaseMetaData metaData; - private final Context context; - - DbMetaData(final Context context, final DatabaseMetaData metaData) { - this.context = context; - this.metaData = metaData; - } - - /** - * Retrieves a description of the tables available in the given catalog. Only - * table descriptions matching the catalog, schema, table name and type criteria - * are returned. They are ordered by {@code TABLE_TYPE}, {@code TABLE_CAT}, - * {@code TABLE_SCHEM} and {@code TABLE_NAME}. - * - * @param catalog a catalog name; must match the catalog name as it is - * stored in the database; "" retrieves those without a - * catalog; {@code null} means that the catalog name - * should not be used to narrow the search - * @param schemaPattern a schema name pattern; must match the schema name as - * it is stored in the database; "" retrieves those - * without a schema; {@code null} means that the schema - * name should not be used to narrow the search - * @param tableNamePattern a table name pattern; must match the table name as it - * is stored in the database - * @param types a list of table types, which must be from the list of - * table types returned from - * {@link DatabaseMetaData#getTableTypes()},to include; - * {@code null} returns all types - * @return table descriptions - * @see DatabaseMetaData#getTables(String, String, String, String[]) - */ - public SimpleResultSet getTables(final String catalog, final String schemaPattern, - final String tableNamePattern, final String[] types) { - return createResultSet(getTablesInternal(catalog, schemaPattern, tableNamePattern, types), - TableMetaData::create); - } - - /** - * Description of a table. - * - * @param tableCatalog table catalog (may be {@code null}) - * @param tableSchema table schema (may be {@code null}) - * @param tableName table name - * @param tableType table type. Typical types are "TABLE", - * "VIEW", "SYSTEM TABLE", "GLOBAL TEMPORARY", - * "LOCAL TEMPORARY", "ALIAS", "SYNONYM". - * @param remarks explanatory comment on the table (may be - * {@code null}) - * @param typeCatalog the types catalog (may be {@code null}) - * @param typeSchema the types schema (may be {@code null}) - * @param typeName type name (may be {@code null}) - * @param selfReferencingColumnName name of the designated "identifier" column - * of a typed table (may be {@code null}) - * @param refGeneration specifies how values in - * SELF_REFERENCING_COL_NAME are created. - * Values are "SYSTEM", "USER", "DERIVED". (may - * be {@code null}) - * @see DatabaseMetaData#getTables(String, String, String, String[]) - */ - public record TableMetaData(String tableCatalog, String tableSchema, String tableName, String tableType, - String remarks, String typeCatalog, String typeSchema, String typeName, String selfReferencingColumnName, - String refGeneration) { - private static TableMetaData create(final ResultSet rs, final int rowNum) throws SQLException { - return new TableMetaData( - rs.getString("TABLE_CAT"), rs.getString("TABLE_SCHEM"), - rs.getString("TABLE_NAME"), - rs.getString("TABLE_TYPE"), - rs.getString("REMARKS"), - rs.getString("TYPE_CAT"), rs.getString("TYPE_SCHEM"), - rs.getString("TYPE_NAME"), - rs.getString("SELF_REFERENCING_COL_NAME"), - rs.getString("REF_GENERATION")); - } - } - - private ResultSet getTablesInternal(final String catalog, final String schemaPattern, final String tableNamePattern, - final String[] types) { - try { - return metaData.getTables(catalog, schemaPattern, tableNamePattern, types); - } catch (final SQLException e) { - throw new UncheckedSQLException("Error getting tables", e); - } - } - - /** - * Retrieves a description of table columns available in the specified catalog. - *

- * Only column descriptions matching the catalog, schema, table and column name - * criteria are returned. They are ordered by - * {@code TABLE_CAT},{@code TABLE_SCHEM}, {@code TABLE_NAME}, and - * {@code ORDINAL_POSITION}. - * - * @param catalog a catalog name; must match the catalog name as it is - * stored in the database; "" retrieves those without a - * catalog; {@code null} means that the catalog name - * should not be used to narrow the search - * @param schemaPattern a schema name pattern; must match the schema name as - * it is stored in the database; "" retrieves those - * without a schema; {@code null} means that the schema - * name should not be used to narrow the search - * @param tableNamePattern a table name pattern; must match the table name as - * it is stored in the database - * @param columnNamePattern a column name pattern; must match the column name as - * it is stored in the database - * @return column descriptions - * @see DatabaseMetaData#getColumns(String, String, String, String) - */ - public SimpleResultSet getColumns(final String catalog, final String schemaPattern, - final String tableNamePattern, final String columnNamePattern) { - return createResultSet(getColumnsInternal(catalog, schemaPattern, tableNamePattern, columnNamePattern), - ColumnMetaData::create); - } - - private ResultSet getColumnsInternal(final String catalog, final String schemaPattern, - final String tableNamePattern, final String columnNamePattern) { - try { - return metaData.getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern); - } catch (final SQLException e) { - throw new UncheckedSQLException("Error getting tables", e); - } - } - - private SimpleResultSet createResultSet(final ResultSet resultSet, final RowMapper rowMapper) { - return new SimpleResultSet<>(context, resultSet, ContextRowMapper.create(rowMapper), () -> { - }); - } - - /** - * Description of a column. - * - * @param tableCatalog table catalog (may be {@code null}) - * @param tableSchema table schema (may be {@code null}) - * @param tableName table name - * @param columnName column name - * @param dataType SQL type from java.sql.Types - * @param typeName Data source dependent type name, for a UDT the type - * name is fully qualified - * @param columnSize column size. - * @param decimalDigits the number of fractional digits. Null is returned - * for data types where DECIMAL_DIGITS is not - * applicable. - * @param numPrecisionRadix Radix (typically either 10 or 2) - * @param nullable is NULL allowed. - *

    - *
  • columnNoNulls - might not allow {@code NULL} - * values - *
  • columnNullable - definitely allows {@code NULL} - * values - *
  • columnNullableUnknown - nullability unknown - *
- * @param remarks comment describing column (may be {@code null}) - * @param columnDef default value for the column, which should be - * interpreted as a string when the value is enclosed - * in single quotes (may be {@code null}) - * @param charOctetLength for char types the maximum number of bytes in the - * column - * @param ordinalPosition index of column in table (starting at 1) - * @param isNullable ISO rules are used to determine the nullability for - * a column. - *
    - *
  • YES --- if the column can include NULLs - *
  • NO --- if the column cannot include NULLs - *
  • empty string --- if the nullability for the - * column is unknown - *
- * @param scopeCatalog catalog of table that is the scope of a reference - * attribute ({@code null} if DATA_TYPE isn't REF) - * @param scopeSchema schema of table that is the scope of a reference - * attribute ({@code null} if the DATA_TYPE isn't REF) - * @param scopeTable table name that this the scope of a reference - * attribute ({@code null} if the DATA_TYPE isn't REF) - * @param sourceDataType source type of a distinct type or user-generated Ref - * type, SQL type from java.sql.Types ({@code null} if - * DATA_TYPE isn't DISTINCT or user-generated REF) - * @param isAutoIncrement Indicates whether this column is auto incremented - *
    - *
  • YES --- if the column is auto incremented - *
  • NO --- if the column is not auto incremented - *
  • empty string --- if it cannot be determined - * whether the column is auto incremented - *
- * @param isGeneratedColumn Indicates whether this is a generated column - *
    - *
  • YES --- if this a generated column - *
  • NO --- if this not a generated column - *
  • empty string --- if it cannot be determined - * whether this is a generated column - *
- * @see DatabaseMetaData#getColumns(String, String, String, String) - */ - public record ColumnMetaData( - String tableCatalog, - String tableSchema, - String tableName, - String columnName, - int dataType, - String typeName, - int columnSize, - int decimalDigits, - int numPrecisionRadix, - String nullable, - String remarks, - String columnDef, - int charOctetLength, - int ordinalPosition, - String isNullable, - String scopeCatalog, - String scopeSchema, - String scopeTable, - short sourceDataType, - String isAutoIncrement, - String isGeneratedColumn) { - - private static ColumnMetaData create(final ResultSet rs, final int rowNum) throws SQLException { - return new ColumnMetaData( - rs.getString("TABLE_CAT"), - rs.getString("TABLE_SCHEM"), - rs.getString("TABLE_NAME"), - rs.getString("COLUMN_NAME"), - rs.getInt("DATA_TYPE"), - rs.getString("TYPE_NAME"), - rs.getInt("COLUMN_SIZE"), - rs.getInt("DECIMAL_DIGITS"), - rs.getInt("NUM_PREC_RADIX"), - rs.getString("NULLABLE"), - rs.getString("REMARKS"), - rs.getString("COLUMN_DEF"), - rs.getInt("CHAR_OCTET_LENGTH"), - rs.getInt("ORDINAL_POSITION"), - rs.getString("IS_NULLABLE"), - rs.getString("SCOPE_CATALOG"), - rs.getString("SCOPE_SCHEMA"), - rs.getString("SCOPE_TABLE"), - rs.getShort("SOURCE_DATA_TYPE"), - rs.getString("IS_AUTOINCREMENT"), - rs.getString("IS_GENERATEDCOLUMN")); - } - } -} diff --git a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java index 6c68951..4a4ecc8 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java @@ -4,6 +4,7 @@ import org.itsallcode.jdbc.batch.*; import org.itsallcode.jdbc.dialect.DbDialect; +import org.itsallcode.jdbc.metadata.DbMetaData; import org.itsallcode.jdbc.resultset.RowMapper; import org.itsallcode.jdbc.resultset.SimpleResultSet; import org.itsallcode.jdbc.resultset.generic.Row; diff --git a/src/main/java/org/itsallcode/jdbc/metadata/ColumnMetaData.java b/src/main/java/org/itsallcode/jdbc/metadata/ColumnMetaData.java new file mode 100644 index 0000000..ad0b006 --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/metadata/ColumnMetaData.java @@ -0,0 +1,195 @@ +package org.itsallcode.jdbc.metadata; + +import java.sql.*; +import java.util.Arrays; + +/** + * Description of a column. + * + * @param tableCatalog table catalog (may be {@code null}) + * @param tableSchema table schema (may be {@code null}) + * @param tableName table name + * @param columnName column name + * @param dataType SQL type + * @param typeName Data source dependent type name, for a UDT the type + * name is fully qualified + * @param columnSize column size. + * @param decimalDigits the number of fractional digits. {@code null} is + * returned for data types where DECIMAL_DIGITS is not + * applicable. + * @param numPrecisionRadix Radix (typically either 10 or 2) + * @param nullable is {@code NULL} allowed. + * @param remarks comment describing column (may be {@code null}) + * @param columnDef default value for the column, which should be + * interpreted as a string when the value is enclosed + * in single quotes (may be {@code null}) + * @param charOctetLength for char types the maximum number of bytes in the + * column + * @param ordinalPosition index of column in table (starting at 1) + * @param isNullable ISO rules are used to determine the nullability for + * a column. + * @param scopeCatalog catalog of table that is the scope of a reference + * attribute ({@code null} if DATA_TYPE isn't REF) + * @param scopeSchema schema of table that is the scope of a reference + * attribute ({@code null} if the DATA_TYPE isn't REF) + * @param scopeTable table name that this the scope of a reference + * attribute ({@code null} if the DATA_TYPE isn't REF) + * @param sourceDataType source type of a distinct type or user-generated Ref + * type, SQL type from java.sql.Types ({@code null} if + * DATA_TYPE isn't DISTINCT or user-generated REF) + * @param isAutoIncrement Indicates whether this column is auto incremented + * @param isGeneratedColumn Indicates whether this is a generated column + * @see DatabaseMetaData#getColumns(String, String, String, String) + */ +public record ColumnMetaData( + String tableCatalog, + String tableSchema, + String tableName, + String columnName, + JDBCType dataType, + String typeName, + int columnSize, + int decimalDigits, + int numPrecisionRadix, + Nullability nullable, + String remarks, + String columnDef, + int charOctetLength, + int ordinalPosition, + ISONullability isNullable, + String scopeCatalog, + String scopeSchema, + String scopeTable, + short sourceDataType, + AutoIncrement isAutoIncrement, + Generated isGeneratedColumn) { + + static ColumnMetaData create(final ResultSet rs, final int rowNum) throws SQLException { + return new ColumnMetaData( + rs.getString("TABLE_CAT"), + rs.getString("TABLE_SCHEM"), + rs.getString("TABLE_NAME"), + rs.getString("COLUMN_NAME"), + JDBCType.valueOf(rs.getInt("DATA_TYPE")), + rs.getString("TYPE_NAME"), + rs.getInt("COLUMN_SIZE"), + rs.getInt("DECIMAL_DIGITS"), + rs.getInt("NUM_PREC_RADIX"), + Nullability.valueOf(rs.getInt("NULLABLE")), + rs.getString("REMARKS"), + rs.getString("COLUMN_DEF"), + rs.getInt("CHAR_OCTET_LENGTH"), + rs.getInt("ORDINAL_POSITION"), + ISONullability.valueOfNullability(rs.getString("IS_NULLABLE")), + rs.getString("SCOPE_CATALOG"), + rs.getString("SCOPE_SCHEMA"), + rs.getString("SCOPE_TABLE"), + rs.getShort("SOURCE_DATA_TYPE"), + AutoIncrement.valueOfAutoIncrement(rs.getString("IS_AUTOINCREMENT")), + Generated.valueOfGenerated(rs.getString("IS_GENERATEDCOLUMN"))); + } + + /** + * Column nullability. + */ + public enum Nullability { + /** Column might not allow {@code NULL} values. */ + NO_NULLS(DatabaseMetaData.columnNoNulls), + /** Column definitely allows {@code NULL} values. */ + NULLABLE(DatabaseMetaData.columnNullable), + /** nullability unknown */ + NULLABLE_UNKNOWN(DatabaseMetaData.columnNullableUnknown); + + private final int value; + + Nullability(final int value) { + this.value = value; + } + + static Nullability valueOf(final int value) { + return Arrays.stream(Nullability.values()) + .filter(n -> n.value == value) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "Unknown value %d for nullability".formatted(value))); + } + } + + /** + * Column ISO nullability. + */ + public enum ISONullability { + /** Column can include NULLs. */ + NO_NULLS("YES"), + /** Column cannot include NULLs. */ + NULLABLE("NO"), + /** Nullability for the column is unknown */ + NULLABLE_UNKNOWN(""); + + private final String value; + + ISONullability(final String value) { + this.value = value; + } + + static ISONullability valueOfNullability(final String value) { + return Arrays.stream(ISONullability.values()) + .filter(n -> n.value.equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "Unknown value %s for nullability".formatted(value))); + } + } + + /** + * Indicates whether a column is auto incremented. + */ + public enum AutoIncrement { + /** Column is auto incremented. */ + AUTO_INCREMENT("YES"), + /** Column is not auto incremented. */ + NO_AUTO_INCREMENT("NO"), + /** It cannot be determined whether the column is auto incremented. */ + UNKNOWN(""); + + private final String value; + + AutoIncrement(final String value) { + this.value = value; + } + + static AutoIncrement valueOfAutoIncrement(final String value) { + return Arrays.stream(AutoIncrement.values()) + .filter(n -> n.value.equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "Unknown value %s for auto increment".formatted(value))); + } + } + + /** + * Indicates whether this is a generated column. + */ + public enum Generated { + /** This a generated column. */ + GENERATED("YES"), + /** This not a generated column. */ + NOT_GENERATED("NO"), + /** It cannot be determined whether this is a generated column. */ + UNKNOWN(""); + + private final String value; + + Generated(final String value) { + this.value = value; + } + + static Generated valueOfGenerated(final String value) { + return Arrays.stream(Generated.values()) + .filter(n -> n.value.equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "Unknown value %s for auto increment".formatted(value))); + } + } +} diff --git a/src/main/java/org/itsallcode/jdbc/metadata/DbMetaData.java b/src/main/java/org/itsallcode/jdbc/metadata/DbMetaData.java new file mode 100644 index 0000000..3b84505 --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/metadata/DbMetaData.java @@ -0,0 +1,108 @@ +package org.itsallcode.jdbc.metadata; + +import java.sql.*; + +import org.itsallcode.jdbc.Context; +import org.itsallcode.jdbc.UncheckedSQLException; +import org.itsallcode.jdbc.resultset.*; + +/** + * A simple wrapper for {@link DatabaseMetaData}. + */ +public class DbMetaData { + + private final DatabaseMetaData metaData; + private final Context context; + + /** + * Create a new instance. + * + * @param context DB context. + * @param metaData metaData object. + */ + public DbMetaData(final Context context, final DatabaseMetaData metaData) { + this.context = context; + this.metaData = metaData; + } + + /** + * Retrieves a description of the tables available in the given catalog. Only + * table descriptions matching the catalog, schema, table name and type criteria + * are returned. They are ordered by {@code TABLE_TYPE}, {@code TABLE_CAT}, + * {@code TABLE_SCHEM} and {@code TABLE_NAME}. + * + * @param catalog a catalog name; must match the catalog name as it is + * stored in the database; "" retrieves those without a + * catalog; {@code null} means that the catalog name + * should not be used to narrow the search + * @param schemaPattern a schema name pattern; must match the schema name as + * it is stored in the database; "" retrieves those + * without a schema; {@code null} means that the schema + * name should not be used to narrow the search + * @param tableNamePattern a table name pattern; must match the table name as it + * is stored in the database + * @param types a list of table types, which must be from the list of + * table types returned from + * {@link DatabaseMetaData#getTableTypes()},to include; + * {@code null} returns all types + * @return table descriptions + * @see DatabaseMetaData#getTables(String, String, String, String[]) + */ + public SimpleResultSet getTables(final String catalog, final String schemaPattern, + final String tableNamePattern, final String[] types) { + return createResultSet(getTablesInternal(catalog, schemaPattern, tableNamePattern, types), + TableMetaData::create); + } + + private ResultSet getTablesInternal(final String catalog, final String schemaPattern, final String tableNamePattern, + final String[] types) { + try { + return metaData.getTables(catalog, schemaPattern, tableNamePattern, types); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error getting tables", e); + } + } + + /** + * Retrieves a description of table columns available in the specified catalog. + *

+ * Only column descriptions matching the catalog, schema, table and column name + * criteria are returned. They are ordered by + * {@code TABLE_CAT},{@code TABLE_SCHEM}, {@code TABLE_NAME}, and + * {@code ORDINAL_POSITION}. + * + * @param catalog a catalog name; must match the catalog name as it is + * stored in the database; "" retrieves those without a + * catalog; {@code null} means that the catalog name + * should not be used to narrow the search + * @param schemaPattern a schema name pattern; must match the schema name as + * it is stored in the database; "" retrieves those + * without a schema; {@code null} means that the schema + * name should not be used to narrow the search + * @param tableNamePattern a table name pattern; must match the table name as + * it is stored in the database + * @param columnNamePattern a column name pattern; must match the column name as + * it is stored in the database + * @return column descriptions + * @see DatabaseMetaData#getColumns(String, String, String, String) + */ + public SimpleResultSet getColumns(final String catalog, final String schemaPattern, + final String tableNamePattern, final String columnNamePattern) { + return createResultSet(getColumnsInternal(catalog, schemaPattern, tableNamePattern, columnNamePattern), + ColumnMetaData::create); + } + + private ResultSet getColumnsInternal(final String catalog, final String schemaPattern, + final String tableNamePattern, final String columnNamePattern) { + try { + return metaData.getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern); + } catch (final SQLException e) { + throw new UncheckedSQLException("Error getting tables", e); + } + } + + private SimpleResultSet createResultSet(final ResultSet resultSet, final RowMapper rowMapper) { + return new SimpleResultSet<>(context, resultSet, ContextRowMapper.create(rowMapper), () -> { + }); + } +} diff --git a/src/main/java/org/itsallcode/jdbc/metadata/TableMetaData.java b/src/main/java/org/itsallcode/jdbc/metadata/TableMetaData.java new file mode 100644 index 0000000..49c6d69 --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/metadata/TableMetaData.java @@ -0,0 +1,43 @@ +package org.itsallcode.jdbc.metadata; + +import java.sql.*; + +/** + * Description of a table. + * + * @param tableCatalog table catalog (may be {@code null}) + * @param tableSchema table schema (may be {@code null}) + * @param tableName table name + * @param tableType table type. Typical types are "TABLE", + * "VIEW", "SYSTEM TABLE", "GLOBAL TEMPORARY", + * "LOCAL TEMPORARY", "ALIAS", "SYNONYM". + * @param remarks explanatory comment on the table (may be + * {@code null}) + * @param typeCatalog the types catalog (may be {@code null}) + * @param typeSchema the types schema (may be {@code null}) + * @param typeName type name (may be {@code null}) + * @param selfReferencingColumnName name of the designated "identifier" column + * of a typed table (may be {@code null}) + * @param refGeneration specifies how values in + * SELF_REFERENCING_COL_NAME are created. + * Values are "SYSTEM", "USER", "DERIVED". (may + * be {@code null}) + * @see DatabaseMetaData#getTables(String, String, String, String[]) + */ +public record TableMetaData(String tableCatalog, String tableSchema, String tableName, String tableType, + String remarks, String typeCatalog, String typeSchema, String typeName, String selfReferencingColumnName, + String refGeneration) { + static TableMetaData create(final ResultSet rs, final int rowNum) throws SQLException { + return new TableMetaData( + rs.getString("TABLE_CAT"), + rs.getString("TABLE_SCHEM"), + rs.getString("TABLE_NAME"), + rs.getString("TABLE_TYPE"), + rs.getString("REMARKS"), + rs.getString("TYPE_CAT"), + rs.getString("TYPE_SCHEM"), + rs.getString("TYPE_NAME"), + rs.getString("SELF_REFERENCING_COL_NAME"), + rs.getString("REF_GENERATION")); + } +} diff --git a/src/test/java/org/itsallcode/jdbc/DbMetaDataITest.java b/src/test/java/org/itsallcode/jdbc/DbMetaDataITest.java index e4f2fda..f8786ee 100644 --- a/src/test/java/org/itsallcode/jdbc/DbMetaDataITest.java +++ b/src/test/java/org/itsallcode/jdbc/DbMetaDataITest.java @@ -2,10 +2,12 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.sql.JDBCType; import java.util.List; -import org.itsallcode.jdbc.DbMetaData.ColumnMetaData; -import org.itsallcode.jdbc.DbMetaData.TableMetaData; +import org.itsallcode.jdbc.metadata.ColumnMetaData; +import org.itsallcode.jdbc.metadata.ColumnMetaData.*; +import org.itsallcode.jdbc.metadata.TableMetaData; import org.itsallcode.jdbc.resultset.SimpleResultSet; import org.junit.jupiter.api.Test; @@ -60,8 +62,10 @@ void getColumns() { .hasSize(451) .first() .isEqualTo(new ColumnMetaData("UNNAMED", "INFORMATION_SCHEMA", "CHECK_CONSTRAINTS", - "CONSTRAINT_CATALOG", 12, "CHARACTER VARYING", 1000000000, 0, - 0, "1", null, null, 0, 0, 1000000000, 1, "YES", null, null, null, (short) 0, "NO", "NO")); + "CONSTRAINT_CATALOG", JDBCType.VARCHAR, "CHARACTER VARYING", 1000000000, 0, 0, + Nullability.NULLABLE, null, + null, 1000000000, 1, ISONullability.NO_NULLS, null, null, null, (short) 0, + AutoIncrement.NO_AUTO_INCREMENT, Generated.NOT_GENERATED)); } } From 8cc39502574a17dc5b9d9ccd1488943a4cebe35e Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sat, 18 Jan 2025 17:30:39 +0100 Subject: [PATCH 5/7] Fix sonar finding --- .../jdbc/metadata/ColumnMetaData.java | 2 +- .../itsallcode/jdbc/metadata/DbMetaData.java | 2 +- .../jdbc/metadata/TableMetaData.java | 2 +- .../jdbc/resultset/ContextRowMapper.java | 13 ++++++++++++ .../jdbc/resultset/SimpleRowMapper.java | 21 +++++++++++++++++++ 5 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/itsallcode/jdbc/resultset/SimpleRowMapper.java diff --git a/src/main/java/org/itsallcode/jdbc/metadata/ColumnMetaData.java b/src/main/java/org/itsallcode/jdbc/metadata/ColumnMetaData.java index ad0b006..075eeae 100644 --- a/src/main/java/org/itsallcode/jdbc/metadata/ColumnMetaData.java +++ b/src/main/java/org/itsallcode/jdbc/metadata/ColumnMetaData.java @@ -64,7 +64,7 @@ public record ColumnMetaData( AutoIncrement isAutoIncrement, Generated isGeneratedColumn) { - static ColumnMetaData create(final ResultSet rs, final int rowNum) throws SQLException { + static ColumnMetaData create(final ResultSet rs) throws SQLException { return new ColumnMetaData( rs.getString("TABLE_CAT"), rs.getString("TABLE_SCHEM"), diff --git a/src/main/java/org/itsallcode/jdbc/metadata/DbMetaData.java b/src/main/java/org/itsallcode/jdbc/metadata/DbMetaData.java index 3b84505..53c6263 100644 --- a/src/main/java/org/itsallcode/jdbc/metadata/DbMetaData.java +++ b/src/main/java/org/itsallcode/jdbc/metadata/DbMetaData.java @@ -101,7 +101,7 @@ private ResultSet getColumnsInternal(final String catalog, final String schemaPa } } - private SimpleResultSet createResultSet(final ResultSet resultSet, final RowMapper rowMapper) { + private SimpleResultSet createResultSet(final ResultSet resultSet, final SimpleRowMapper rowMapper) { return new SimpleResultSet<>(context, resultSet, ContextRowMapper.create(rowMapper), () -> { }); } diff --git a/src/main/java/org/itsallcode/jdbc/metadata/TableMetaData.java b/src/main/java/org/itsallcode/jdbc/metadata/TableMetaData.java index 49c6d69..ca3d6a2 100644 --- a/src/main/java/org/itsallcode/jdbc/metadata/TableMetaData.java +++ b/src/main/java/org/itsallcode/jdbc/metadata/TableMetaData.java @@ -27,7 +27,7 @@ public record TableMetaData(String tableCatalog, String tableSchema, String tableName, String tableType, String remarks, String typeCatalog, String typeSchema, String typeName, String selfReferencingColumnName, String refGeneration) { - static TableMetaData create(final ResultSet rs, final int rowNum) throws SQLException { + static TableMetaData create(final ResultSet rs) throws SQLException { return new TableMetaData( rs.getString("TABLE_CAT"), rs.getString("TABLE_SCHEM"), diff --git a/src/main/java/org/itsallcode/jdbc/resultset/ContextRowMapper.java b/src/main/java/org/itsallcode/jdbc/resultset/ContextRowMapper.java index d4c0f2b..3cac805 100644 --- a/src/main/java/org/itsallcode/jdbc/resultset/ContextRowMapper.java +++ b/src/main/java/org/itsallcode/jdbc/resultset/ContextRowMapper.java @@ -53,6 +53,19 @@ private static RowMapper generic(final DbDialect dialect, final ColumnVal return new GenericRowMapper<>(dialect, converter); } + /** + * Creates a new new {@link ContextRowMapper} from a {@link SimpleRowMapper}. + *

+ * Use this if the mapper doesn't need the {@link Context}. + * + * @param generic row type + * @param mapper the simple row mapper + * @return a new {@link ContextRowMapper} + */ + static ContextRowMapper create(final SimpleRowMapper mapper) { + return (context, resultSet, rowNum) -> mapper.mapRow(resultSet); + } + /** * Creates a new new {@link ContextRowMapper} from a {@link RowMapper}. *

diff --git a/src/main/java/org/itsallcode/jdbc/resultset/SimpleRowMapper.java b/src/main/java/org/itsallcode/jdbc/resultset/SimpleRowMapper.java new file mode 100644 index 0000000..a5135df --- /dev/null +++ b/src/main/java/org/itsallcode/jdbc/resultset/SimpleRowMapper.java @@ -0,0 +1,21 @@ +package org.itsallcode.jdbc.resultset; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Converts a single row from a {@link ResultSet} to a generic row type. + * + * @param generic row type + */ +@FunctionalInterface +public interface SimpleRowMapper { + /** + * Converts a single row from a {@link ResultSet} to a generic row type. + * + * @param resultSet result set + * @return the converted row + * @throws SQLException if accessing the result set fails + */ + T mapRow(ResultSet resultSet) throws SQLException; +} From c1cef98134bd4d78ee6ed3786f244d18685aa865 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sat, 18 Jan 2025 18:02:19 +0100 Subject: [PATCH 6/7] Add unit tests --- .../itsallcode/jdbc/ConnectionWrapper.java | 2 +- .../jdbc/metadata/ColumnMetaData.java | 6 +- .../itsallcode/jdbc/metadata/DbMetaData.java | 8 +-- .../jdbc/ConnectionWrapperTest.java | 14 +++++ .../jdbc/metadata/ColumnMetaDataTest.java | 60 +++++++++++++++++++ .../jdbc/{ => metadata}/DbMetaDataITest.java | 6 +- .../jdbc/metadata/DbMetaDataTest.java | 55 +++++++++++++++++ 7 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 src/test/java/org/itsallcode/jdbc/metadata/ColumnMetaDataTest.java rename src/test/java/org/itsallcode/jdbc/{ => metadata}/DbMetaDataITest.java (97%) create mode 100644 src/test/java/org/itsallcode/jdbc/metadata/DbMetaDataTest.java diff --git a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java index 35daab3..28c5712 100644 --- a/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java +++ b/src/main/java/org/itsallcode/jdbc/ConnectionWrapper.java @@ -118,7 +118,7 @@ private DatabaseMetaData getMetaDataInternal() { try { return connection.getMetaData(); } catch (final SQLException e) { - throw new UncheckedSQLException("Failed to get metadata ", e); + throw new UncheckedSQLException("Failed to get metadata", e); } } diff --git a/src/main/java/org/itsallcode/jdbc/metadata/ColumnMetaData.java b/src/main/java/org/itsallcode/jdbc/metadata/ColumnMetaData.java index 075eeae..232576b 100644 --- a/src/main/java/org/itsallcode/jdbc/metadata/ColumnMetaData.java +++ b/src/main/java/org/itsallcode/jdbc/metadata/ColumnMetaData.java @@ -137,7 +137,7 @@ static ISONullability valueOfNullability(final String value) { .filter(n -> n.value.equals(value)) .findFirst() .orElseThrow(() -> new IllegalArgumentException( - "Unknown value %s for nullability".formatted(value))); + "Unknown value '%s' for ISO nullability".formatted(value))); } } @@ -163,7 +163,7 @@ static AutoIncrement valueOfAutoIncrement(final String value) { .filter(n -> n.value.equals(value)) .findFirst() .orElseThrow(() -> new IllegalArgumentException( - "Unknown value %s for auto increment".formatted(value))); + "Unknown value '%s' for auto increment".formatted(value))); } } @@ -189,7 +189,7 @@ static Generated valueOfGenerated(final String value) { .filter(n -> n.value.equals(value)) .findFirst() .orElseThrow(() -> new IllegalArgumentException( - "Unknown value %s for auto increment".formatted(value))); + "Unknown value '%s' for column generated".formatted(value))); } } } diff --git a/src/main/java/org/itsallcode/jdbc/metadata/DbMetaData.java b/src/main/java/org/itsallcode/jdbc/metadata/DbMetaData.java index 53c6263..bdaea70 100644 --- a/src/main/java/org/itsallcode/jdbc/metadata/DbMetaData.java +++ b/src/main/java/org/itsallcode/jdbc/metadata/DbMetaData.java @@ -50,7 +50,7 @@ public DbMetaData(final Context context, final DatabaseMetaData metaData) { */ public SimpleResultSet getTables(final String catalog, final String schemaPattern, final String tableNamePattern, final String[] types) { - return createResultSet(getTablesInternal(catalog, schemaPattern, tableNamePattern, types), + return wrapResultSet(getTablesInternal(catalog, schemaPattern, tableNamePattern, types), TableMetaData::create); } @@ -88,7 +88,7 @@ private ResultSet getTablesInternal(final String catalog, final String schemaPat */ public SimpleResultSet getColumns(final String catalog, final String schemaPattern, final String tableNamePattern, final String columnNamePattern) { - return createResultSet(getColumnsInternal(catalog, schemaPattern, tableNamePattern, columnNamePattern), + return wrapResultSet(getColumnsInternal(catalog, schemaPattern, tableNamePattern, columnNamePattern), ColumnMetaData::create); } @@ -97,11 +97,11 @@ private ResultSet getColumnsInternal(final String catalog, final String schemaPa try { return metaData.getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern); } catch (final SQLException e) { - throw new UncheckedSQLException("Error getting tables", e); + throw new UncheckedSQLException("Error getting columns", e); } } - private SimpleResultSet createResultSet(final ResultSet resultSet, final SimpleRowMapper rowMapper) { + private SimpleResultSet wrapResultSet(final ResultSet resultSet, final SimpleRowMapper rowMapper) { return new SimpleResultSet<>(context, resultSet, ContextRowMapper.create(rowMapper), () -> { }); } diff --git a/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java b/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java index 747a49e..377e7ac 100644 --- a/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java +++ b/src/test/java/org/itsallcode/jdbc/ConnectionWrapperTest.java @@ -280,6 +280,20 @@ void isClosedFails() throws SQLException { .hasMessage("Failed to get closed state: expected"); } + @Test + void getMetaData() { + assertThat(testee().getMetaData()).isNotNull(); + } + + @Test + void getMetaDataFails() throws SQLException { + when(connectionMock.getMetaData()).thenThrow(new SQLException("expected")); + final ConnectionWrapper testee = testee(); + assertThatThrownBy(testee::getMetaData) + .isInstanceOf(UncheckedSQLException.class) + .hasMessage("Failed to get metadata: expected"); + } + @Test void getOriginalConnection() { assertThat(testee().getOriginalConnection()).isSameAs(connectionMock); diff --git a/src/test/java/org/itsallcode/jdbc/metadata/ColumnMetaDataTest.java b/src/test/java/org/itsallcode/jdbc/metadata/ColumnMetaDataTest.java new file mode 100644 index 0000000..8228e4e --- /dev/null +++ b/src/test/java/org/itsallcode/jdbc/metadata/ColumnMetaDataTest.java @@ -0,0 +1,60 @@ +package org.itsallcode.jdbc.metadata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.sql.DatabaseMetaData; + +import org.itsallcode.jdbc.metadata.ColumnMetaData.*; +import org.junit.jupiter.api.Test; + +class ColumnMetaDataTest { + + @Test + void nullability() { + assertThat(Nullability.valueOf(DatabaseMetaData.columnNoNulls)).isEqualTo(Nullability.NO_NULLS); + } + + @Test + void nullabilityInvalid() { + assertThatThrownBy(() -> Nullability.valueOf(3)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unknown value 3 for nullability"); + } + + @Test + void isoNullability() { + assertThat(ISONullability.valueOfNullability("NO")).isEqualTo(ISONullability.NULLABLE); + } + + @Test + void isoNullabilityInvalid() { + assertThatThrownBy(() -> ISONullability.valueOfNullability("unknown")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unknown value 'unknown' for ISO nullability"); + } + + @Test + void autoIncrement() { + assertThat(AutoIncrement.valueOfAutoIncrement("NO")).isEqualTo(AutoIncrement.NO_AUTO_INCREMENT); + } + + @Test + void autoIncrementInvalid() { + assertThatThrownBy(() -> AutoIncrement.valueOfAutoIncrement("unknown")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unknown value 'unknown' for auto increment"); + } + + @Test + void generated() { + assertThat(Generated.valueOfGenerated("NO")).isEqualTo(Generated.NOT_GENERATED); + } + + @Test + void generatedUnknown() { + assertThatThrownBy(() -> Generated.valueOfGenerated("unknown")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unknown value 'unknown' for column generated"); + } +} diff --git a/src/test/java/org/itsallcode/jdbc/DbMetaDataITest.java b/src/test/java/org/itsallcode/jdbc/metadata/DbMetaDataITest.java similarity index 97% rename from src/test/java/org/itsallcode/jdbc/DbMetaDataITest.java rename to src/test/java/org/itsallcode/jdbc/metadata/DbMetaDataITest.java index f8786ee..7774ad1 100644 --- a/src/test/java/org/itsallcode/jdbc/DbMetaDataITest.java +++ b/src/test/java/org/itsallcode/jdbc/metadata/DbMetaDataITest.java @@ -1,13 +1,13 @@ -package org.itsallcode.jdbc; +package org.itsallcode.jdbc.metadata; import static org.assertj.core.api.Assertions.assertThat; import java.sql.JDBCType; import java.util.List; -import org.itsallcode.jdbc.metadata.ColumnMetaData; +import org.itsallcode.jdbc.H2TestFixture; +import org.itsallcode.jdbc.SimpleConnection; import org.itsallcode.jdbc.metadata.ColumnMetaData.*; -import org.itsallcode.jdbc.metadata.TableMetaData; import org.itsallcode.jdbc.resultset.SimpleResultSet; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/itsallcode/jdbc/metadata/DbMetaDataTest.java b/src/test/java/org/itsallcode/jdbc/metadata/DbMetaDataTest.java new file mode 100644 index 0000000..de6f417 --- /dev/null +++ b/src/test/java/org/itsallcode/jdbc/metadata/DbMetaDataTest.java @@ -0,0 +1,55 @@ +package org.itsallcode.jdbc.metadata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import org.itsallcode.jdbc.Context; +import org.itsallcode.jdbc.UncheckedSQLException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DbMetaDataTest { + @Mock + DatabaseMetaData metaDataMock; + + @Test + void getTables() { + assertThat(testee().getTables("catalog", "schema", "table", new String[] { "type" })).isNotNull(); + } + + @Test + void getTablesFails() throws SQLException { + when(metaDataMock.getTables("catalog", "schema", "table", new String[] { "type" })) + .thenThrow(new SQLException("expected")); + final DbMetaData testee = testee(); + assertThatThrownBy(() -> testee.getTables("catalog", "schema", "table", new String[] { "type" })) + .isInstanceOf(UncheckedSQLException.class) + .hasMessage("Error getting tables: expected"); + } + + @Test + void getColumns() { + assertThat(testee().getColumns("catalog", "schema", "table", "column")).isNotNull(); + } + + @Test + void getColumnsFails() throws SQLException { + when(metaDataMock.getColumns("catalog", "schema", "table", "column")) + .thenThrow(new SQLException("expected")); + final DbMetaData testee = testee(); + assertThatThrownBy(() -> testee.getColumns("catalog", "schema", "table", "column")) + .isInstanceOf(UncheckedSQLException.class) + .hasMessage("Error getting columns: expected"); + } + + DbMetaData testee() { + return new DbMetaData(Context.builder().build(), metaDataMock); + } +} From 41e4f755043a146fb943637553c6bcab0bff88a6 Mon Sep 17 00:00:00 2001 From: kaklakariada Date: Sat, 18 Jan 2025 19:03:24 +0100 Subject: [PATCH 7/7] Add parameterized tests --- .../jdbc/metadata/ColumnMetaDataTest.java | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/test/java/org/itsallcode/jdbc/metadata/ColumnMetaDataTest.java b/src/test/java/org/itsallcode/jdbc/metadata/ColumnMetaDataTest.java index 8228e4e..312b1fa 100644 --- a/src/test/java/org/itsallcode/jdbc/metadata/ColumnMetaDataTest.java +++ b/src/test/java/org/itsallcode/jdbc/metadata/ColumnMetaDataTest.java @@ -3,16 +3,17 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import java.sql.DatabaseMetaData; - import org.itsallcode.jdbc.metadata.ColumnMetaData.*; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class ColumnMetaDataTest { - @Test - void nullability() { - assertThat(Nullability.valueOf(DatabaseMetaData.columnNoNulls)).isEqualTo(Nullability.NO_NULLS); + @ParameterizedTest + @CsvSource({ "0,NO_NULLS", "1,NULLABLE", "2,NULLABLE_UNKNOWN" }) + void nullability(final int value, final Nullability expected) { + assertThat(Nullability.valueOf(value)).isEqualTo(expected); } @Test @@ -22,9 +23,10 @@ void nullabilityInvalid() { .hasMessage("Unknown value 3 for nullability"); } - @Test - void isoNullability() { - assertThat(ISONullability.valueOfNullability("NO")).isEqualTo(ISONullability.NULLABLE); + @ParameterizedTest + @CsvSource({ "YES,NO_NULLS", "NO,NULLABLE", "'',NULLABLE_UNKNOWN" }) + void isoNullability(final String value, final ISONullability expected) { + assertThat(ISONullability.valueOfNullability(value)).isEqualTo(expected); } @Test @@ -34,9 +36,10 @@ void isoNullabilityInvalid() { .hasMessage("Unknown value 'unknown' for ISO nullability"); } - @Test - void autoIncrement() { - assertThat(AutoIncrement.valueOfAutoIncrement("NO")).isEqualTo(AutoIncrement.NO_AUTO_INCREMENT); + @ParameterizedTest + @CsvSource({ "YES,AUTO_INCREMENT", "NO,NO_AUTO_INCREMENT", "'',UNKNOWN" }) + void autoIncrement(final String value, final AutoIncrement expected) { + assertThat(AutoIncrement.valueOfAutoIncrement(value)).isEqualTo(expected); } @Test @@ -46,9 +49,10 @@ void autoIncrementInvalid() { .hasMessage("Unknown value 'unknown' for auto increment"); } - @Test - void generated() { - assertThat(Generated.valueOfGenerated("NO")).isEqualTo(Generated.NOT_GENERATED); + @ParameterizedTest + @CsvSource({ "YES,GENERATED", "NO,NOT_GENERATED", "'',UNKNOWN" }) + void generated(final String value, final Generated expected) { + assertThat(Generated.valueOfGenerated(value)).isEqualTo(expected); } @Test