All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nuiton.db.meta.DatabaseMetaBuilder Maven / Gradle / Ivy

The newest version!
package org.nuiton.db.meta;

/*-
 * #%L
 * Nuiton DB Meta
 * %%
 * Copyright (C) 2019 Nuiton
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * .
 * #L%
 */

import com.google.common.base.CaseFormat;
import com.google.common.base.Converter;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Streams;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reflections.Reflections;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Arnaud Thimel (Code Lutin)
 * @since 0.1
 */
public class DatabaseMetaBuilder {

    private static final Log log = LogFactory.getLog(DatabaseMetaBuilder.class);

    public static final Pattern POSSIBLE_VALUES_PATTERN = Pattern.compile("(Valeurs possibles :){1} (.*)");

    protected static Cache metaCountCache;

    protected final Consumer worker;

    protected final String packageForEnumResolver;

    public DatabaseMetaBuilder(Consumer worker, String packageForEnumResolver) {
        this.worker = worker;
        this.packageForEnumResolver = packageForEnumResolver;
    }

    public DatabaseMetaBuilder(Connection sqlConnection, String packageForEnumResolver) {
        this(sqlWork -> {
            try {
                sqlWork.execute(sqlConnection);
            } catch (SQLException e) {
                log.error("Unable to do SqlWork", e);
            }
        }, packageForEnumResolver);
    }

    public DatabaseMetaBuilder(Connection sqlConnection) {
        this(sqlConnection, null);
    }

    protected Cache getMetaCountCache() {
        if (metaCountCache == null) {
            metaCountCache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build();
        }
        return metaCountCache;
    }

    protected void doWork(SqlWork work) {
        worker.accept(work);
    }

    protected ImmutableSet getTableNames(String schema, String type) {
        ImmutableSet.Builder builder = ImmutableSet.builder();
        doWork(connection -> {
            DatabaseMetaData metaData = connection.getMetaData();
            try (ResultSet resultSet = metaData.getTables(null, schema, null, new String[]{type})) {
                while (resultSet.next()) {
                    // Les colonnes (PG) : table_cat, table_schem, table_name, table_type, remarks
                    String name = resultSet.getString("TABLE_NAME");
                    builder.add(name);
                }
            }
        });
        return builder.build();
    }

    protected ImmutableSet getColumnNames(String schema, String tableName) {
        ImmutableSet.Builder builder = ImmutableSet.builder();

        doWork(connection -> {
            DatabaseMetaData metaData = connection.getMetaData();
            try (ResultSet resultSet = metaData.getColumns(null, schema, tableName, null)) {
                // Les colonnes (PG) : 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_CATLOG, SCOPE_SCHEMA, SCOPE_TABLE, SOURCE_DATA_TYPE, IS_AUTOINCREMENT
//                ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
//                int columnCount = resultSetMetaData.getColumnCount();
//                String[] columns = new String[columnCount];
//                for (int i = 0; i readTableMetas(String type,
                                                      String schema,
                                                      Map comments,
                                                      Set primaryKeys,
                                                      Set uniqueColumns,
                                                      Multimap foreignKeys,
                                                      ImmutableList customTypes) {

//        doWork(connection -> {
//            DatabaseMetaData metaData = connection.getMetaData();
//            ResultSet resultSet = metaData.getTables(null, schema, null, null);
//            ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
//            int columnCount = resultSetMetaData.getColumnCount();
//            String[] columns = new String[columnCount];
//            for (int i = 0; i tableMetas = ImmutableList.builder();
        for (String tableName : getTableNames(schema, type)) {


            ImmutableSet columnNames = getColumnNames(schema, tableName);

            doWork(connection -> {

                List columns = Lists.newArrayList();

                for (String tableColumnName : columnNames) {

                    DatabaseMetaData metaData = connection.getMetaData();
                    try (ResultSet resultSet = metaData.getColumns(null, schema, tableName, tableColumnName)) {
                        // Les colonnes (PG) : 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_CATLOG, SCOPE_SCHEMA, SCOPE_TABLE, SOURCE_DATA_TYPE, IS_AUTOINCREMENT
//                        ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
//                    int columnCount = resultSetMetaData.getColumnCount();
//                    String[] metas = new String[columnCount];
//                    for (int i = 0; i < columnCount; i++) {
//                        String metaName = resultSetMetaData.getColumnName(i + 1);
//                        //                    System.out.println(metaName);
//                        metas[i] = metaName;
//                    }
                        while (resultSet.next()) {
                            String dataType = resultSet.getString("TYPE_NAME");
                            Optional size = Optional.empty();
                            Optional customTypeMeta = customTypes.stream().filter(customType -> customType.getName().equals(dataType)).findAny();
                            if (!customTypeMeta.isPresent()) {
                                size = Optional.of(resultSet.getInt("COLUMN_SIZE"));
                            }
                            boolean nullable = resultSet.getInt("NULLABLE") == 1;
                            ColumnRef columnRef = asCommentKey(tableName, tableColumnName);
                            Optional comment = Optional.ofNullable(comments.get(columnRef));
                            boolean primaryKey = primaryKeys.contains(columnRef);
                            boolean unique = uniqueColumns.contains(columnRef);
                            if (!comment.isPresent() && log.isWarnEnabled()) {
                                log.warn(String.format("Pas de commentaire pour la colonne %s", columnRef.pretty()));
                            }
                            ImmutableSet keys = getForeignKeys(foreignKeys, columnRef);

                            Optional> possibleValues = Optional.empty();
                            if (comment.isPresent()) {
                                Matcher matcher = POSSIBLE_VALUES_PATTERN.matcher(comment.get());
                                if (matcher.find()) {
                                    String values = matcher.group(2);
                                    Map map;
                                    if (values.contains("=")) {
                                        map = Splitter.on(",")
                                                .omitEmptyStrings()
                                                .trimResults()
                                                .withKeyValueSeparator("=")
                                                .split(values);
                                        if (log.isWarnEnabled()) {
                                            for (Map.Entry entry : map.entrySet()) {
                                                if (entry.getKey().equals(entry.getValue())) {
                                                    String message = String.format(
                                                            "Valeur '%s' suspecte (identique clé=valeur) pour la colonne %s",
                                                            entry.getKey(),
                                                            columnRef.pretty());
                                                    log.warn(message);
                                                }
                                            }
                                        }
                                    } else {
                                        Iterable tabPossibleValues = Splitter.on(",")
                                                .omitEmptyStrings()
                                                .trimResults()
                                                .split(values);
                                        map = Maps.newHashMap();
                                        for (String possibleValue:tabPossibleValues) {
                                            map.put(possibleValue, null);
                                        }
                                    }
                                    possibleValues = Optional.of(map);
                                }

                                if (customTypeMeta.isPresent() && log.isWarnEnabled()) {
                                    if (possibleValues.isPresent()) {
                                        for (String value : possibleValues.get().keySet()) {
                                            if (!customTypeMeta.get().getValues().contains(value)) {
                                                String message = String.format(
                                                        "La valeur '%s' n'est pas attendue selon le type %s pour la colonne %s",
                                                        value,
                                                        customTypeMeta.get().getName(),
                                                        columnRef.pretty());
                                                log.warn(message);
                                            }
                                        }
                                    } else {
                                        String message = String.format(
                                                "La liste des valeurs n'est pas détaillée pour la colonne %s de type %s",
                                                columnRef.pretty(),
                                                customTypeMeta.get().getName());
                                        log.warn(message);
                                    }
                                }

                            }

                            ColumnMeta column = ImmutableColumnMeta.builder()
                                    .name(tableColumnName)
                                    .type(dataType)
                                    .customType(customTypeMeta)
                                    .length(size)
                                    .nullable(nullable)
                                    .comment(comment)
                                    .isPrimaryKey(primaryKey)
                                    .isUnique(unique)
                                    .isForeignKey(!keys.isEmpty())
                                    .foreignKeyColumns(keys)
                                    .possibleValues(possibleValues)
                                    .build();
                            columns.add(column);
//                        System.out.println(String.format("*********** %s %s (%d) %s ***********", tableColumnName, dataType, size, nullable));
                            //                    for (int i = 0; i comment = Optional.ofNullable(comments.get(commentKey));
                if (!comment.isPresent() && log.isWarnEnabled()) {
                    log.warn(String.format("Pas de commentaire pour la table/vue %s", tableName));
                }

                TableMeta tableMeta = ImmutableTableMeta.builder()
                        .name(tableName)
                        .isView("VIEW".equals(type))
                        .columns(columns)
                        .count(Optional.ofNullable(count))
                        .comment(comment)
                        .build();
                tableMetas.add(tableMeta);

            });

        }
        ImmutableList result = tableMetas.build();
        return result;
    }


    protected ImmutableSet getForeignKeys(Multimap foreignKeys, ColumnRef columnRef) {
        Collection keys = foreignKeys.get(columnRef);
        if (CollectionUtils.isEmpty(keys)) {
            return ImmutableSet.of();
        }
        ImmutableSet result = keys.stream()
                .map(ColumnRef::pretty)
                .collect(ImmutableSet.toImmutableSet());
        return result;
    }

    protected ColumnRef asCommentKey(String tableName, String columnName) {
        Optional right = Optional.ofNullable(StringUtils.trimToNull(columnName));
        if (right.isPresent()) {
            return ColumnRef.forColumn(tableName, columnName);
        } else {
            return ColumnRef.forTable(tableName);
        }
    }

    protected ColumnRef asProcedureKey(String procedureName, String argTypesString) {
        Optional args = Optional.ofNullable(StringUtils.trimToNull(argTypesString));
        String procedureKey = procedureName;
        if (args.isPresent()) {
            procedureKey += String.format(" (%s)", args.get());
        }
        ColumnRef procedureRef = asCommentKey(procedureKey, null);
        return procedureRef;
    }

    /**
     * Dans la Map renvoyée, la clé est constituée d'une Pair de TABLE_NAME, COLUMN_NAME. Sachant que COLUMN_NAME peut
     * être null s'il s'agit d'un commentaire sur la table.
     */
    protected Map readTableAndViewComments(String schema) {
        ImmutableMap.Builder builder = ImmutableMap.builder();
        doWork(connection -> {
            String sql = String.format("SELECT " +
                    "    t.table_type AS table_type, " +
                    "    t.table_name AS table, " +
                    "    c.column_name AS colonne, " +
                    "    d.description AS commentaire " +
                    "FROM ( " +
                    "        SELECT " +
                    "            table_schema, " +
                    "            table_name, " +
                    "            (table_schema || '.' || table_name)::regclass::oid AS objoid, " +
                    "            table_type " +
                    "        FROM " +
                    "            information_schema.tables " +
                    "        WHERE " +
                    "            table_schema = '%s') t " +
                    "    INNER JOIN pg_catalog.pg_description d  " +
                    "        ON d.objoid = t.objoid " +
                    "    LEFT JOIN information_schema.columns c  " +
                    "        ON c.table_schema = t.table_schema " +
                    "        AND c.table_name = t.table_name " +
                    "        AND c.ordinal_position = d.objsubid " +
                    "    ORDER BY " +
                    "        t.table_name, " +
                    "        d.objsubid; ", schema);
            try (PreparedStatement statement = connection.prepareStatement(sql)) {
                try (ResultSet resultSet = statement.executeQuery()) {
                    while (resultSet.next()) {
//                        String tableType = resultSet.getString("table_type");
                        String table = resultSet.getString("table");
                        String colonne = resultSet.getString("colonne");
                        String commentaire = resultSet.getString("commentaire");
                        ColumnRef key = asCommentKey(table, colonne);
                        builder.put(key, commentaire);
                    }
                }
            }
        });
        ImmutableMap result = builder.build();
        return result;
    }

    /**
     * Dans la Map renvoyée, la clé est constituée d'une Pair de PROCEDURE_NAME, ATTRIBUTE_NAME. Sachant que
     * ATTRIBUTE_NAME peut être null s'il s'agit d'un commentaire sur la procédure.
     */
    protected Map readProcedureComments(String schema) {
        ImmutableMap.Builder builder = ImmutableMap.builder();
        doWork(connection -> {
            String sql = String.format(
                    "SELECT" +
                    "    p.proname AS nom, " +
                    "    p.proargtypes AS arguments, " +
                    "    d.description AS commentaire " +
                    "FROM " +
                    "    pg_catalog.pg_namespace n " +
                    "    JOIN pg_catalog.pg_proc p ON p.pronamespace = n.oid " +
                    "    INNER JOIN pg_catalog.pg_description d ON d.objoid = p.oid " +
                    "WHERE " +
                    "    n.nspname = '%s' " +
                    " ; "
                    , schema);
            try (PreparedStatement statement = connection.prepareStatement(sql)) {
                try (ResultSet resultSet = statement.executeQuery()) {
                    while (resultSet.next()) {
                        String table = resultSet.getString("nom");
                        String argTypesString = resultSet.getString("arguments");
                        String commentaire = resultSet.getString("commentaire");
                        ColumnRef key = asProcedureKey(table, argTypesString);
                        builder.put(key, commentaire);
                    }
                }
            }
        });
        ImmutableMap result = builder.build();
        return result;
    }

    protected ImmutableList readProcedureMetas(String schema) {

        Map comments = readProcedureComments(schema);

        ImmutableList.Builder builder = ImmutableList.builder();

        Map types = new HashMap<>();
        doWork(connection -> {
            String sql = " select oid, t.typname from pg_type t ";
            try (PreparedStatement statement = connection.prepareStatement(sql)) {
                try (ResultSet resultSet = statement.executeQuery()) {
                    while (resultSet.next()) {
                        int id = resultSet.getInt("oid");
                        String type = resultSet.getString("typname");
                        types.put(id, type);
                    }
                }
            }
        });

        doWork(connection -> {
            String sql = String.format("select p.proname, p.proargnames, p.proargtypes " +
                    " from pg_proc p " +
                    " inner join pg_namespace n " +
                    "   on n.oid = p.pronamespace " +
                    " where " +
                    // On limite à l'utilisateur autre que l'utilisateur système (> 10), à moins que ce soit l'utilisateur courant
                    " (p.proowner > 10 or p.proowner = (select usesysid FROM pg_user u where u.usename = (select * from current_user))) " +
                    " and n.nspname='%s'", schema);
            try (PreparedStatement statement = connection.prepareStatement(sql)) {
                try (ResultSet resultSet = statement.executeQuery()) {
                    while (resultSet.next()) {
                        String name = resultSet.getString("proname");
                        String argTypesString = resultSet.getString("proargtypes");
                        List args = new LinkedList<>();
                        if (StringUtils.isNotEmpty(argTypesString)) {

                            ImmutableList argTypes = Streams.stream(Splitter.on(' ').split(argTypesString))
                                    .map(Integer::valueOf)
                                    .map(types::get)
                                    .collect(ImmutableList.toImmutableList());

                            String proArgs = Strings.nullToEmpty(resultSet.getString("proargnames"));
                            // {source_exercise_id,destination_exercise_id}
                            if (proArgs.startsWith("{") && proArgs.endsWith("}")) {
                                proArgs = proArgs.substring(0, proArgs.length() - 1).substring(1);
                            }
                            ImmutableList argNames = ImmutableList.copyOf(Splitter.on(',').omitEmptyStrings().split(proArgs));

                            for (int i=0; i comment = Optional.ofNullable(comments.get(commentKey));
                        ProcedureMeta procedureMeta = ImmutableProcedureMeta.builder()
                                .name(name)
                                .comment(comment)
                                .args(args)
                                .build();
                        builder.add(procedureMeta);
                    }
                }
            }
        });

        ImmutableList result = builder.build();
        return result;
    }

    /**
     * Dans la Multimap renvoyée, la clé est constituée d'une Pair de TABLE_NAME, COLUMN_NAME. Les valeurs sont
     * également des Pair de TABLE_NAME, COLUMN_NAME représentant la colonne référencée par la FK
     */
    protected Multimap readForeignKeys(String schema) {
        LinkedHashMultimap result = LinkedHashMultimap.create();

        doWork(connection -> {
            String sql = String.format("select " +
                    "    contrainte.conname as foreign_key_name, " +
                    "    local_class.relname as table, " +
                    "    local_attribute.attname as column, " +
                    "    foreign_class.relname as foreign_table, " +
                    "    foreign_attribute.attname as foreign_column " +
                    " from " +
                    "   (select " +
                    "        unnest(con1.conkey) as \"parent\", " +
                    "        unnest(con1.confkey) as \"child\", " +
                    "        con1.confrelid, " +
                    "        con1.conrelid, " +
                    "        con1.conname " +
                    "    from " +
                    "        pg_class cl " +
                    "        join pg_namespace ns " +
                    "            on cl.relnamespace = ns.oid " +
                    "        join pg_constraint con1 " +
                    "            on con1.conrelid = cl.oid " +
                    "    where " +
                    "        ns.nspname = '%s' " +
                    "        and con1.contype = 'f' " +
                    "   ) contrainte " +
                    "   join pg_class foreign_class " +
                    "       on foreign_class.oid = contrainte.confrelid " +
                    "   join pg_attribute foreign_attribute " +
                    "       on foreign_attribute.attrelid = contrainte.confrelid " +
                    "       and foreign_attribute.attnum = contrainte.child " +
                    "   join pg_class local_class " +
                    "   \t   on local_class.oid = contrainte.conrelid " +
                    "   join pg_attribute local_attribute " +
                    "       on local_attribute.attrelid = contrainte.conrelid " +
                    "       and local_attribute.attnum = contrainte.parent " +
                    "   order by local_class.relname asc, " +
                    "            local_attribute.attname asc " +
                    "       ;\n", schema);
            try (PreparedStatement statement = connection.prepareStatement(sql)) {
                try (ResultSet resultSet = statement.executeQuery()) {
                    while (resultSet.next()) {
                        String table = resultSet.getString("table");
                        String colonne = resultSet.getString("column");
                        String foreignTable = resultSet.getString("foreign_table");
                        String foreignColonne = resultSet.getString("foreign_column");
                        ColumnRef source = ColumnRef.forColumn(table, colonne);
                        ColumnRef reference = ColumnRef.forColumn(foreignTable, foreignColonne);
                        result.put(source, reference);
                    }
                }
            }
        });
        return result;
    }

    /**
     * Renvoie un ensemble de Pair de TABLE_NAME, COLUMN_NAME représentant les clés primaires de la base.
     */
    protected ImmutableSet readPrimaryKeys(String schema) {
        ImmutableSet.Builder builder = ImmutableSet.builder();

        doWork(connection -> {
            String sql = String.format("select " +
                    "    con.conname as primary_key_name, " +
                    "    table_class.relname as table, " +
                    "    column_attribute.attname as column " +
                    "from " +
                    "   (select " +
                    "        unnest(con1.conkey) as \"parent\", " +
                    "        con1.conrelid, " +
                    "        con1.conname " +
                    "    from " +
                    "        pg_class cl " +
                    "        join pg_namespace ns " +
                    "            on cl.relnamespace = ns.oid " +
                    "        join pg_constraint con1 " +
                    "            on con1.conrelid = cl.oid " +
                    "    where " +
                    "        ns.nspname = '%s' " +
                    "        and con1.contype = 'p' " +
                    "   ) con " +
                    "   join pg_class table_class " +
                    "       on table_class.oid = con.conrelid " +
                    "   join pg_attribute column_attribute " +
                    "       on column_attribute.attrelid = con.conrelid " +
                    "       and column_attribute.attnum = con.parent " +
                    "   ; ", schema);
            try (PreparedStatement statement = connection.prepareStatement(sql)) {
                try (ResultSet resultSet = statement.executeQuery()) {
                    while (resultSet.next()) {
                        String table = resultSet.getString("table");
                        String colonne = resultSet.getString("column");
                        ColumnRef source = ColumnRef.forColumn(table, colonne);
                        builder.add(source);
                    }
                }
            }
        });
        ImmutableSet result = builder.build();
        return result;
    }

    /**
     * Renvoie un ensemble de Pair de TABLE_NAME, COLUMN_NAME représentant les colonnes avec une contrainte d'unicité.
     */
    protected ImmutableSet readUniqueColumns(String schema) {
        ImmutableSet.Builder builder = ImmutableSet.builder();

        doWork(connection -> {
            String sql = String.format("select " +
                    "    con.conname as primary_key_name, " +
                    "    table_class.relname as table, " +
                    "    column_attribute.attname as column " +
                    "from " +
                    "   (select " +
                    "        unnest(con1.conkey) as \"parent\", " +
                    "        con1.conrelid, " +
                    "        con1.conname " +
                    "    from " +
                    "        pg_class cl " +
                    "        join pg_namespace ns " +
                    "            on cl.relnamespace = ns.oid " +
                    "        join pg_constraint con1 " +
                    "            on con1.conrelid = cl.oid " +
                    "    where " +
                    "        ns.nspname = '%s' " +
                    "        and con1.contype = 'u' " +
                    "   ) con " +
                    "   join pg_class table_class " +
                    "       on table_class.oid = con.conrelid " +
                    "   join pg_attribute column_attribute " +
                    "       on column_attribute.attrelid = con.conrelid " +
                    "       and column_attribute.attnum = con.parent " +
                    "   ; ", schema);
            try (PreparedStatement statement = connection.prepareStatement(sql)) {
                try (ResultSet resultSet = statement.executeQuery()) {
                    while (resultSet.next()) {
                        String table = resultSet.getString("table");
                        String colonne = resultSet.getString("column");
                        ColumnRef source = ColumnRef.forColumn(table, colonne);
                        builder.add(source);
                    }
                }
            }
        });
        ImmutableSet result = builder.build();
        return result;
    }

    protected ImmutableList readTableMetas(String schema, ImmutableList customTypes) {
        Map comments = readTableAndViewComments(schema);
        Set primaryKeys = readPrimaryKeys(schema);
        Set uniqueColumns = readUniqueColumns(schema);
        Multimap foreignKeys = readForeignKeys(schema);
        ImmutableList result = readTableMetas("TABLE", schema, comments, primaryKeys, uniqueColumns, foreignKeys, customTypes);
        return result;
    }

    protected ImmutableList readViewMetas(String schema, ImmutableList customTypes) {
        Map comments = readTableAndViewComments(schema);
        Set primaryKeys = readPrimaryKeys(schema);
        Set uniqueColumns = readUniqueColumns(schema);
        Multimap foreignKeys = readForeignKeys(schema);
        ImmutableList result = readTableMetas("VIEW", schema, comments, primaryKeys, uniqueColumns, foreignKeys, customTypes);
        return result;
    }

    protected Map readCustomTypesComments(String schema) {
        Map result = new HashMap<>();

        doWork(connection -> {
            String sql = String.format(
                    "  SELECT t.typname AS type," +
                            "        d.description AS comment" +
                            " FROM pg_type t " +
                            " INNER JOIN pg_catalog.pg_namespace n " +
                            "   ON n.oid = t.typnamespace" +
                            "   AND n.nspname = '%s'" +
                            " INNER JOIN pg_catalog.pg_description d" +
                            "   ON d.objoid = (n.nspname || '.' || t.typname)::regtype::oid" +
                            "   AND d.objsubid = 0 -- Pour ne pas avoir les commentaires sur les valeurs des types" +
                            " WHERE (t.typrelid = 0 " +
                            "       OR " +
                            "         (SELECT c.relkind = 'c' " +
                            "          FROM pg_catalog.pg_class c " +
                            "          WHERE c.oid = t.typrelid)) " +
                            " AND NOT EXISTS " +
                            "     (SELECT 1 " +
                            "      FROM pg_catalog.pg_type el " +
                            "      WHERE el.oid = t.typelem " +
                            "        AND el.typarray = t.oid) ; ", schema);
            try (PreparedStatement statement = connection.prepareStatement(sql)) {
                try (ResultSet resultSet = statement.executeQuery()) {
                    while (resultSet.next()) {
                        String name = resultSet.getString("type");
                        String comment = resultSet.getString("comment");
                        if (StringUtils.isNotEmpty(comment)) {
                            result.put(name, comment);
                        }

                    }
                }
            }
        });
        return result;
    }

    protected ImmutableList readCustomTypes(String schema, Function> enumResolver) {

        Map comments = readCustomTypesComments(schema);

        ImmutableList.Builder builder = ImmutableList.builder();
        doWork(connection -> {
            String sql = String.format(
                    " SELECT t.typname AS type " +
                    " FROM pg_type t " +
                    " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace " +
                    " WHERE (t.typrelid = 0 " +
                    "       OR " +
                    "         (SELECT c.relkind = 'c' " +
                    "          FROM pg_catalog.pg_class c " +
                    "          WHERE c.oid = t.typrelid)) " +
                    " AND NOT EXISTS " +
                    "     (SELECT 1 " +
                    "      FROM pg_catalog.pg_type el " +
                    "      WHERE el.oid = t.typelem " +
                    "        AND el.typarray = t.oid) " +
                    " AND n.nspname = '%s';", schema);
            try (PreparedStatement statement = connection.prepareStatement(sql)) {
                try (ResultSet resultSet = statement.executeQuery()) {
                    while (resultSet.next()) {
                        String name = resultSet.getString("type");
                        Optional comment = Optional.ofNullable(comments.get(name));
                        Class foundClass = enumResolver.apply(name);
                        Optional> enumMatching = Optional.ofNullable(foundClass);
                        List values = new LinkedList<>();

                        if (enumMatching.isPresent()) {
                            Class aClass = enumMatching.get();
                            try {
                                Method valuesMethod = aClass.getMethod("values");
                                Object invokeResult = valuesMethod.invoke(null);
                                Enum[] result = (Enum[]) invokeResult;
                                Arrays.stream(result)
                                        .map(Enum::name)
                                        .forEach(values::add);
                            } catch (Exception eee) {
                                log.error("Unable to read enum values", eee);
                            }
                        } else if (log.isWarnEnabled()) {
                            log.warn("No enum found for name: " + name);
                        }

                        ImmutableCustomTypeMeta customType = ImmutableCustomTypeMeta.builder()
                                .name(name)
                                .enumEquivalence(enumMatching)
                                .values(values)
                                .comment(comment)
                                .build();
                        builder.add(customType);
                    }
                }
            }
        });
        ImmutableList result = builder.build();
        return result;
    }

    protected Function> packageEnumResolver() {

        Set> enums;
        if (StringUtils.isNotEmpty(packageForEnumResolver)) {
            enums = new Reflections(packageForEnumResolver).getSubTypesOf(Enum.class);
        } else {
            enums = ImmutableSet.of();
        }
        Converter camelToUnderscore = CaseFormat.UPPER_CAMEL.converterTo(CaseFormat.LOWER_UNDERSCORE);
        ImmutableMap> enumsIndex =
                Maps.uniqueIndex(enums, e -> camelToUnderscore.convert(e.getSimpleName()));

        Function> result = s -> enumsIndex.get(s.toLowerCase());
        return result;
    }

    public DatabaseMeta buildMeta(String schema) {
        ImmutableList customTypes = readCustomTypes(schema, packageEnumResolver());
        ImmutableList tables = readTableMetas(schema, customTypes);
        ImmutableList views = readViewMetas(schema, customTypes);
        ImmutableList procedures = readProcedureMetas(schema);

        DatabaseMeta result = ImmutableDatabaseMeta.builder()
                .schema(schema)
                .customTypes(customTypes)
                .tables(tables)
                .views(views)
                .procedures(procedures)
                .build();
        return result;
    }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy