001package org.itsallcode.jdbc.metadata;
002
003import java.sql.*;
004import java.util.Arrays;
005
006/**
007 * Description of a column.
008 * 
009 * @param tableCatalog      table catalog (may be {@code null})
010 * @param tableSchema       table schema (may be {@code null})
011 * @param tableName         table name
012 * @param columnName        column name
013 * @param dataType          SQL type
014 * @param typeName          Data source dependent type name, for a UDT the type
015 *                          name is fully qualified
016 * @param columnSize        column size.
017 * @param decimalDigits     the number of fractional digits. {@code null} is
018 *                          returned for data types where DECIMAL_DIGITS is not
019 *                          applicable.
020 * @param numPrecisionRadix Radix (typically either 10 or 2)
021 * @param nullable          is {@code NULL} allowed.
022 * @param remarks           comment describing column (may be {@code null})
023 * @param columnDef         default value for the column, which should be
024 *                          interpreted as a string when the value is enclosed
025 *                          in single quotes (may be {@code null})
026 * @param charOctetLength   for char types the maximum number of bytes in the
027 *                          column
028 * @param ordinalPosition   index of column in table (starting at 1)
029 * @param isNullable        ISO rules are used to determine the nullability for
030 *                          a column.
031 * @param scopeCatalog      catalog of table that is the scope of a reference
032 *                          attribute ({@code null} if DATA_TYPE isn't REF)
033 * @param scopeSchema       schema of table that is the scope of a reference
034 *                          attribute ({@code null} if the DATA_TYPE isn't REF)
035 * @param scopeTable        table name that this the scope of a reference
036 *                          attribute ({@code null} if the DATA_TYPE isn't REF)
037 * @param sourceDataType    source type of a distinct type or user-generated Ref
038 *                          type, SQL type from java.sql.Types ({@code null} if
039 *                          DATA_TYPE isn't DISTINCT or user-generated REF)
040 * @param isAutoIncrement   Indicates whether this column is auto incremented
041 * @param isGeneratedColumn Indicates whether this is a generated column
042 * @see DatabaseMetaData#getColumns(String, String, String, String)
043 */
044public record ColumnMetaData(
045        String tableCatalog,
046        String tableSchema,
047        String tableName,
048        String columnName,
049        JDBCType dataType,
050        String typeName,
051        int columnSize,
052        int decimalDigits,
053        int numPrecisionRadix,
054        Nullability nullable,
055        String remarks,
056        String columnDef,
057        int charOctetLength,
058        int ordinalPosition,
059        ISONullability isNullable,
060        String scopeCatalog,
061        String scopeSchema,
062        String scopeTable,
063        short sourceDataType,
064        AutoIncrement isAutoIncrement,
065        Generated isGeneratedColumn) {
066
067    static ColumnMetaData create(final ResultSet rs) throws SQLException {
068        return new ColumnMetaData(
069                rs.getString("TABLE_CAT"),
070                rs.getString("TABLE_SCHEM"),
071                rs.getString("TABLE_NAME"),
072                rs.getString("COLUMN_NAME"),
073                JDBCType.valueOf(rs.getInt("DATA_TYPE")),
074                rs.getString("TYPE_NAME"),
075                rs.getInt("COLUMN_SIZE"),
076                rs.getInt("DECIMAL_DIGITS"),
077                rs.getInt("NUM_PREC_RADIX"),
078                Nullability.valueOf(rs.getInt("NULLABLE")),
079                rs.getString("REMARKS"),
080                rs.getString("COLUMN_DEF"),
081                rs.getInt("CHAR_OCTET_LENGTH"),
082                rs.getInt("ORDINAL_POSITION"),
083                ISONullability.valueOfNullability(rs.getString("IS_NULLABLE")),
084                rs.getString("SCOPE_CATALOG"),
085                rs.getString("SCOPE_SCHEMA"),
086                rs.getString("SCOPE_TABLE"),
087                rs.getShort("SOURCE_DATA_TYPE"),
088                AutoIncrement.valueOfAutoIncrement(rs.getString("IS_AUTOINCREMENT")),
089                Generated.valueOfGenerated(rs.getString("IS_GENERATEDCOLUMN")));
090    }
091
092    /**
093     * Column nullability.
094     */
095    public enum Nullability {
096        /** Column might not allow {@code NULL} values. */
097        NO_NULLS(DatabaseMetaData.columnNoNulls),
098        /** Column definitely allows {@code NULL} values. */
099        NULLABLE(DatabaseMetaData.columnNullable),
100        /** nullability unknown */
101        NULLABLE_UNKNOWN(DatabaseMetaData.columnNullableUnknown);
102
103        private final int value;
104
105        Nullability(final int value) {
106            this.value = value;
107        }
108
109        static Nullability valueOf(final int value) {
110            return Arrays.stream(Nullability.values())
111                    .filter(n -> n.value == value)
112                    .findFirst()
113                    .orElseThrow(() -> new IllegalArgumentException(
114                            "Unknown value %d for nullability".formatted(value)));
115        }
116    }
117
118    /**
119     * Column ISO nullability.
120     */
121    public enum ISONullability {
122        /** Column can include NULLs. */
123        NO_NULLS("YES"),
124        /** Column cannot include NULLs. */
125        NULLABLE("NO"),
126        /** Nullability for the column is unknown */
127        NULLABLE_UNKNOWN("");
128
129        private final String value;
130
131        ISONullability(final String value) {
132            this.value = value;
133        }
134
135        static ISONullability valueOfNullability(final String value) {
136            return Arrays.stream(ISONullability.values())
137                    .filter(n -> n.value.equals(value))
138                    .findFirst()
139                    .orElseThrow(() -> new IllegalArgumentException(
140                            "Unknown value '%s' for ISO nullability".formatted(value)));
141        }
142    }
143
144    /**
145     * Indicates whether a column is auto incremented.
146     */
147    public enum AutoIncrement {
148        /** Column is auto incremented. */
149        AUTO_INCREMENT("YES"),
150        /** Column is not auto incremented. */
151        NO_AUTO_INCREMENT("NO"),
152        /** It cannot be determined whether the column is auto incremented. */
153        UNKNOWN("");
154
155        private final String value;
156
157        AutoIncrement(final String value) {
158            this.value = value;
159        }
160
161        static AutoIncrement valueOfAutoIncrement(final String value) {
162            return Arrays.stream(AutoIncrement.values())
163                    .filter(n -> n.value.equals(value))
164                    .findFirst()
165                    .orElseThrow(() -> new IllegalArgumentException(
166                            "Unknown value '%s' for auto increment".formatted(value)));
167        }
168    }
169
170    /**
171     * Indicates whether this is a generated column.
172     */
173    public enum Generated {
174        /** This a generated column. */
175        GENERATED("YES"),
176        /** This not a generated column. */
177        NOT_GENERATED("NO"),
178        /** It cannot be determined whether this is a generated column. */
179        UNKNOWN("");
180
181        private final String value;
182
183        Generated(final String value) {
184            this.value = value;
185        }
186
187        static Generated valueOfGenerated(final String value) {
188            return Arrays.stream(Generated.values())
189                    .filter(n -> n.value.equals(value))
190                    .findFirst()
191                    .orElseThrow(() -> new IllegalArgumentException(
192                            "Unknown value '%s' for column generated".formatted(value)));
193        }
194    }
195}