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

com.heliorm.sql.postgres.PostgresModeller Maven / Gradle / Ivy

package com.heliorm.sql.postgres;

import com.heliorm.sql.BinaryColumn;
import com.heliorm.sql.BitColumn;
import com.heliorm.sql.BooleanColumn;
import com.heliorm.sql.Column;
import com.heliorm.sql.Database;
import com.heliorm.sql.DecimalColumn;
import com.heliorm.sql.EnumColumn;
import com.heliorm.sql.Index;
import com.heliorm.sql.SetColumn;
import com.heliorm.sql.SqlModeller;
import com.heliorm.sql.SqlModellerException;
import com.heliorm.sql.StringColumn;
import com.heliorm.sql.Table;

import java.sql.Connection;
import java.sql.JDBCType;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.lang.String.format;

/**
 * An implementation of the SQL modeller that deals with PostgreSQL syntax.
 */
public final class PostgresModeller extends SqlModeller {
    /**
     * Create a new modeller with the given connection supplier and driver.
     *
     * @param supplier The connection supplier
     */
    public PostgresModeller(Supplier supplier) {
        super(supplier);
    }

    @Override
    public void modifyColumn(Column column) throws SqlModellerException {
        if (column instanceof EnumColumn ec) {
            modifyEnumColumn(ec);
        } else {
            super.modifyColumn(column);
        }
    }

    @Override
    public void modifyIndex(Index index) throws SqlModellerException {
        try (Connection con = con(); Statement stmt = con.createStatement()) {
            stmt.executeUpdate(makeModifyIndexQuery(index));
        } catch (SQLException ex) {
            throw new SqlModellerException(format("Error modifying index '%s' in table '%s' (%s)", index.getName(), index.getTable().getName(), ex.getMessage()));
        }
    }

    @Override
    public boolean supportsSet() {
        return false;
    }

    @Override
    protected boolean isEnumColumn(String columnName, JDBCType jdbcType, String typeName) throws SqlModellerException {
        if (jdbcType == JDBCType.VARCHAR) {
            try (Connection con = con(); Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(makeReadEnumQuery(typeName))) {
                return rs.next();
            } catch (SQLException ex) {
                throw new SqlModellerException(format("Error reading enum values from databases (%s)", ex.getMessage()), ex);
            }
        }
        return false;
    }

    @Override
    protected boolean typesAreCompatible(Column one, Column other) {
        if (one instanceof BooleanColumn) {
            if (other instanceof BitColumn) {
                return ((BitColumn) other).getBits() == 1;
            }
            return other instanceof BooleanColumn;
        }
        if (one instanceof BitColumn) {
            if (other instanceof BitColumn) {
                return ((BitColumn) one).getBits() == ((BitColumn) other).getBits();
            }
            return other instanceof BooleanColumn && ((BitColumn) one).getBits() == 1;
        }
        if (one instanceof StringColumn) {
            if (other instanceof StringColumn) {
                return actualTextLength((StringColumn) one) == actualTextLength((StringColumn) other);
            }
            return false;
        }
        if (one instanceof DecimalColumn) {
            if (other instanceof DecimalColumn) {
                if (one.getJdbcType() == JDBCType.DOUBLE) {
                    if (other.getJdbcType() == JDBCType.DOUBLE) {
                        return true;
                    }
                }
                return ((DecimalColumn) one).getPrecision() == ((DecimalColumn) other).getPrecision()
                        && ((DecimalColumn) one).getScale() == ((DecimalColumn) other).getScale();
            }
            return other.getJdbcType() == JDBCType.NUMERIC;
        }
        if (one instanceof BinaryColumn) {
            return other instanceof BinaryColumn;
        }
        if (one.getJdbcType() == JDBCType.NUMERIC) {
            switch (other.getJdbcType()) {
                case NUMERIC:
                case DECIMAL:
                    return true;
            }
        }
        return one.getJdbcType() == other.getJdbcType();
    }

    @Override
    protected boolean isSetColumn(String columnName, JDBCType jdbcType, String typeName) {
        return false;
    }

    @Override
    protected String makeReadSetQuery(SetColumn sqlSetColumn) throws SqlModellerException {
        throw new SqlModellerException("SET data types are not supported for PostgreSQL");
    }

    @Override
    protected Set extractSetValues(String string) {
        return null;
    }

    @Override
    public List makeModifyColumnQuery(Column column) throws SqlModellerException {
        StringBuilder sql = new StringBuilder();
        sql.append(format("ALTER TABLE %s", getTableName(column.getTable())));
        sql.append(format("ALTER %s DROP DEFAULT", getColumnName(column)));
        sql.append(format(",ALTER %s TYPE %s USING(%s::text::%s)",
                getColumnName(column), createBasicType(column),
                getColumnName(column),
                typeName(column)));
        if (!column.isNullable()) {
            sql.append(format(",ALTER %s SET NOT NULL", getColumnName(column)));
        } else {
            sql.append(format(",ALTER %s DROP NOT NULL", getColumnName(column)));
        }
        return List.of(sql.toString());
    }

    @Override
    protected List makeModifyColumnQuery(Column current, Column changed) throws SqlModellerException {
        return makeModifyColumnQuery(changed);
    }

    @Override
    protected String getDatabaseName(Database database) {
        return format("\"%s\"", database.getName());
    }

    @Override
    protected String getTableName(Table table) {
        return format("\"%s\".\"public\".\"%s\"", table.getDatabase().getName(), table.getName());
    }

    @Override
    protected String getCreateType(Column column) throws SqlModellerException {
        StringBuilder type = new StringBuilder();
        type.append(createBasicType(column));
        if (column.isKey()) {
            type.append(" PRIMARY KEY");
        }
        if (!column.isNullable()) {
            type.append(" NOT NULL");
        }
        if (column.getDefault() != null)
            type.append(format(" DEFAULT '%s'", column.getDefault()));
        return type.toString();
    }

    @Override
    protected String makeRemoveIndexQuery(Index index) {
        return format("DROP INDEX IF EXISTS %s", getIndexName(index));
    }

    @Override
    protected String makeModifyIndexQuery(Index index) {
        return makeRemoveIndexQuery(index) + ";" + makeAddIndexQuery(index);
    }

    @Override
    protected String getColumnName(Column column) {
        return format("\"%s\"", column.getName());
    }

    @Override
    protected String getIndexName(Index index) {
        return format("\"%s\"", index.getName());
    }

    @Override
    protected String makeAddColumnQuery(Column column) throws SqlModellerException {
        StringBuilder buf = new StringBuilder();
        if (column instanceof EnumColumn) {
            buf.append(makeAddEnumTypeQuery((EnumColumn) column));
        } else if (column instanceof SetColumn) {
            throw new SqlModellerException("SET data types are not supported for PostgreSQL");
        }
        buf.append(format("ALTER TABLE %s ADD COLUMN %s %s",
                getTableName(column.getTable()),
                getColumnName(column),
                getCreateType(column)));
        return buf.toString();
    }

    @Override
    protected String makeCreateTableQuery(Table table) throws SqlModellerException {
        StringBuilder head = new StringBuilder();
        StringJoiner body = new StringJoiner(",");
        for (Column column : table.getColumns()) {
            if (column instanceof EnumColumn) {
                head.append(makeAddEnumTypeQuery((EnumColumn) column));
            }
            if (column instanceof SetColumn) {
                throw new SqlModellerException("SET data types are not supported for PostgreSQL");
            }
            body.add(format("%s %s", getColumnName(column), getCreateType(column)));
        }
        StringBuilder sql = new StringBuilder(head.toString());
        sql.append(format("CREATE TABLE %s (", getTableName(table)));
        sql.append(body);
        sql.append(")");
        for (Index index : table.getIndexes()) {
            sql.append(";\n");
            sql.append(makeAddIndexQuery(index));
        }
        return sql.toString();
    }

    @Override
    protected String makeRenameIndexQuery(Index current, Index changed) {
        return format("ALTER INDEX %s RENAME to %s",
                getIndexName(current),
                getIndexName(changed));
    }

    @Override
    protected Set readEnumValues(EnumColumn column) throws SqlModellerException {
        try (Connection con = con(); Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(makeReadEnumQuery(getSqlTypeName(column)))) {
            if (rs.next()) {
                return Stream.of(rs.getString("enum_value").split(","))
                        .map(String::trim)
                        .collect(Collectors.toSet());
            }
            throw new SqlModellerException(format("No enum values found for column %s in table %s ", column.getName(), column.getTable().getName()));
        } catch (SQLException ex) {
            throw new SqlModellerException(format("Error reading enum values (%s)", ex.getMessage()), ex);
        }
    }


    @Override
    protected String extractDefault(String text) {
        int idx = text.indexOf("::");
        if (idx > 0) {
            return text.substring(0, idx).replace("'", "");
        }
        return text;
    }

    /**
     * Read the SQL type name for the give column from the database meta data.
     *
     * @param column The column
     * @return The type name
     */
    private String getSqlTypeName(Column column) throws SqlModellerException {
        try (var con = con()) {
            var dbm = con.getMetaData();
            try (var rs = dbm.getColumns(column.getTable().getDatabase().getName(), null, column.getTable().getName(), column.getName())) {
                if (rs.next()) {
                    return rs.getString("TYPE_NAME");
                }
            }
            throw new SqlModellerException(format("Column %s found in table %s ", column.getName(), column.getTable().getName()));
        } catch (SQLException ex) {
            throw new SqlModellerException(format("Error determining SQL type name for column %s in table %s ", column.getName(), column.getTable().getName()));
        }
    }

    /**
     * Modify an enum colum in a PostgreSQL specific way.
     *
     * @param column The column to modify
     * @throws SqlModellerException Thrown if it goes worng
     */
    private void modifyEnumColumn(EnumColumn column) throws SqlModellerException {
        var want = column.getEnumValues();
        var have = readEnumValues(column);
        if (!want.equals(have)) {
            var query = new StringJoiner(";");
            query.add(format("ALTER TYPE %s RENAME TO %s_old", typeName(column), typeName(column)));
            query.add(makeAddEnumTypeQuery(column));
            query.add(format("ALTER TABLE %s COLUMN %s TYPE %s USING %s::text::%s",
                    getTableName(column.getTable()),
                    getColumnName(column),
                    typeName(column),
                    getColumnName(column),
                    typeName(column)));
            try (var con = con(); var stmt = con.createStatement()) {
                stmt.executeUpdate(query.toString());
            } catch (SQLException e) {
                throw new SqlModellerException(e.getMessage(), e);
            }
        }
    }

    /**
     * Generate an SQL statement to modify a PostgreSQL enum type.
     *
     * @param column The column
     * @return The SQL
     */
    private String makeAddEnumTypeQuery(EnumColumn column) {
        var typeName = typeName(column);
        var buf = new StringJoiner("\n");
        buf.add("DO $$");
        buf.add("BEGIN");
        buf.add(format("    IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = '%s') THEN", typeName));
        buf.add(format("        CREATE TYPE \"%s\" AS ENUM(", typeName));
        buf.add(column.getEnumValues().stream()
                .map(v -> "'" + v + "'")
                .collect(Collectors.joining(",")));
        buf.add(");");
        buf.add("    END IF;");
        buf.add("END$$;");
        return buf.toString();
    }

    /**
     * Create the basic type declaration for a column excluding annotations like keys and nullability
     *
     * @param column The column
     * @return The type declaration
     */
    private String createBasicType(Column column) throws SqlModellerException {
        var type = new StringBuilder();
        String typeName;
        switch (column) {
            case EnumColumn ignored -> typeName = "\"" + typeName(column) + "\"";
            case SetColumn ignored -> throw new SqlModellerException("SET data types are not supported for Postgres");
            case StringColumn stringColumn -> {
                int length = stringColumn.getLength();
                if (length > 65535) {
                    typeName = "TEXT";
                } else {
                    typeName = format("VARCHAR(%d)", length);
                }
            }
            case DecimalColumn ignored -> typeName = switch (column.getJdbcType()) {
                case DOUBLE -> "DOUBLE PRECISION";
                case FLOAT -> format("FLOAT(%d)", ((DecimalColumn) column).getPrecision());
                case NUMERIC, DECIMAL ->
                        format("DECIMAL(%d,%d)", ((DecimalColumn) column).getPrecision(), ((DecimalColumn) column).getScale());
                default ->
                        throw new SqlModellerException(format("Unexpected JDBC type %s in decimal column", column.getJdbcType()));
            };
            case BinaryColumn ignored -> typeName = "BYTEA";
            default -> {
                switch (column.getJdbcType()) {
                    case TINYINT -> {
                        if (column.isKey() && column.isAutoIncrement()) {
                            typeName = "SERIAL";
                        } else {
                            typeName = "TINYINT";
                        }
                    }
                    case SMALLINT -> {
                        if (column.isKey() && column.isAutoIncrement()) {
                            typeName = "SERIAL";
                        } else {
                            typeName = "SMALLINT";
                        }
                    }
                    case INTEGER -> {
                        if (column.isKey() && column.isAutoIncrement()) {
                            typeName = "SERIAL";
                        } else {
                            typeName = "INTEGER";
                        }
                    }
                    case BIGINT -> {
                        if (column.isKey() && column.isAutoIncrement()) {
                            typeName = "BIGSERIAL";
                        } else {
                            typeName = "BIGINT";
                        }
                    }
                    case DOUBLE -> typeName = "DOUBLE PRECISION";
                    default -> typeName = column.getJdbcType().getName();
                }
            }
        }
        type.append(typeName);
        return type.toString();
    }

    /**
     * Determine the PostgreSQL type name for a column.
     *
     * @param column The column
     * @return The type name
     */
    private String typeName(Column column) {
        if (column instanceof EnumColumn) {
            return format("%s_%s", column.getTable().getName(), column.getName());
        }
        switch (column.getJdbcType()) {
            case TINYINT -> {
                if (column.isKey() && column.isAutoIncrement()) {
                    return "SERIAL";
                } else {
                    return "TINYINT";
                }
            }
            case SMALLINT -> {
                if (column.isKey() && column.isAutoIncrement()) {
                    return "SERIAL";
                } else {
                    return "SMALLINT";
                }
            }
            case INTEGER -> {
                if (column.isKey() && column.isAutoIncrement()) {
                    return "SERIAL";
                } else {
                    return "INTEGER";
                }
            }
            case BIGINT -> {
                if (column.isKey() && column.isAutoIncrement()) {
                    return "BIGSERIAL";
                } else {
                    return "BIGINT";
                }
            }
            case LONGVARCHAR -> {
                if (column instanceof StringColumn) {
                    int length = ((StringColumn) column).getLength();
                    if (length > 65535) {
                        return "TEXT";
                    } else {
                        return format("VARCHAR(%d)", length);
                    }
                }
            }
            default -> {
                return column.getJdbcType().getName();
            }
        }
        return column.getJdbcType().getName();
    }

    /**
     * Generate an SQL query that reads the enum data for the given type name.
     *
     * @param typeName The type name
     * @return The SQL query
     */
    private String makeReadEnumQuery(String typeName) {
        return "select n.nspname as enum_schema,  \n" +
                "    t.typname as enum_name,\n" +
                "    string_agg(e.enumlabel, ', ') as enum_value\n" +
                "from pg_type t \n" +
                "    join pg_enum e on t.oid = e.enumtypid  \n" +
                "    join pg_catalog.pg_namespace n ON n.oid = t.typnamespace\n" +
                "    where t.typname = '" + typeName + "' " +
                "group by enum_schema, enum_name;";
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy