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

ru.curs.celesta.dbutils.adaptors.ddl.DdlGenerator Maven / Gradle / Ivy

The newest version!
package ru.curs.celesta.dbutils.adaptors.ddl;

import ru.curs.celesta.CelestaException;
import ru.curs.celesta.DBType;
import ru.curs.celesta.dbutils.adaptors.DBAdaptor;
import ru.curs.celesta.dbutils.adaptors.column.ColumnDefinerFactory;
import ru.curs.celesta.dbutils.meta.DbColumnInfo;
import ru.curs.celesta.dbutils.meta.DbIndexInfo;
import ru.curs.celesta.event.TriggerQuery;
import ru.curs.celesta.score.BasicTable;
import ru.curs.celesta.score.BinaryColumn;
import ru.curs.celesta.score.BooleanColumn;
import ru.curs.celesta.score.Column;
import ru.curs.celesta.score.Count;
import ru.curs.celesta.score.DateTimeColumn;
import ru.curs.celesta.score.DecimalColumn;
import ru.curs.celesta.score.Expr;
import ru.curs.celesta.score.FloatingColumn;
import ru.curs.celesta.score.ForeignKey;
import ru.curs.celesta.score.Grain;
import ru.curs.celesta.score.Index;
import ru.curs.celesta.score.IntegerColumn;
import ru.curs.celesta.score.MaterializedView;
import ru.curs.celesta.score.ParameterizedView;
import ru.curs.celesta.score.SQLGenerator;
import ru.curs.celesta.score.SequenceElement;
import ru.curs.celesta.score.StringColumn;
import ru.curs.celesta.score.Sum;
import ru.curs.celesta.score.TableElement;
import ru.curs.celesta.score.VersionedElement;
import ru.curs.celesta.score.View;
import ru.curs.celesta.score.ZonedDateTimeColumn;


import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.sql.Connection;
import java.sql.SQLException;

import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static ru.curs.celesta.dbutils.adaptors.constants.CommonConstants.ALTER_TABLE;

/**
 * Base class for SQL generation of data definition.
 */
public abstract class DdlGenerator {

    static final Map>> CELESTA_TYPES_COLUMN_CLASSES = new HashMap<>();

    static {
        CELESTA_TYPES_COLUMN_CLASSES.put(IntegerColumn.CELESTA_TYPE, IntegerColumn.class);
        CELESTA_TYPES_COLUMN_CLASSES.put(FloatingColumn.CELESTA_TYPE, FloatingColumn.class);
        CELESTA_TYPES_COLUMN_CLASSES.put(DecimalColumn.CELESTA_TYPE, DecimalColumn.class);
        CELESTA_TYPES_COLUMN_CLASSES.put(BooleanColumn.CELESTA_TYPE, BooleanColumn.class);
        CELESTA_TYPES_COLUMN_CLASSES.put(StringColumn.VARCHAR, StringColumn.class);
        CELESTA_TYPES_COLUMN_CLASSES.put(BinaryColumn.CELESTA_TYPE, BinaryColumn.class);
        CELESTA_TYPES_COLUMN_CLASSES.put(DateTimeColumn.CELESTA_TYPE, DateTimeColumn.class);
        CELESTA_TYPES_COLUMN_CLASSES.put(ZonedDateTimeColumn.CELESTA_TYPE, ZonedDateTimeColumn.class);
    }

    DBAdaptor dmlAdaptor;

    Map>> triggers = new HashMap<>();

    public DdlGenerator(DBAdaptor dmlAdaptor) {
        this.dmlAdaptor = dmlAdaptor;
    }

    /**
     * Generates SQL for schema creation in the DB.
     *
     * @param name  schema names
     */
    Optional createSchema(String name) {
        String sql = String.format("create schema \"%s\"", name);
        return Optional.of(sql);
    }


    /**
     * Generates SQL for sequence creation in the DB.
     *
     * @param s sequence definition
     */
    List createSequence(SequenceElement s) {
        String sql = String.format(
            "CREATE SEQUENCE %s %s",
            sequenceString(s.getGrain().getName(), s.getName()),
            generateArgumentsForCreateSequenceExpression(s)
        );

        return Collections.singletonList(sql);
    }

    /**
     * Generates SQL for dropping view in the schema.
     *
     * @param schemaName  schema name
     * @param viewName  view name
     */
    String dropView(String schemaName, String viewName) {
        String sql = String.format("DROP VIEW %s", tableString(schemaName, viewName));
        return sql;
    }

    abstract List dropParameterizedView(
            String schemaName, String viewName, Connection conn
    );

    abstract List dropIndex(Grain g, DbIndexInfo dBIndexInfo);

    final String dropFk(String schemaName, String tableName, String fkName) {
        String sql = String.format(
                "alter table %s drop constraint \"%s\"",
                tableString(schemaName, tableName), fkName
        );

        return sql;
    }

    /**
     * Adds drop trigger statements to drop Foreign Key.
     * @param fkName Name of the foreign key being dropped
     */
    List dropUpdateRule(String fkName) {
        return Collections.emptyList();
    }

    final String dropTrigger(TriggerQuery query) {
        this.forgetTrigger(query);
        return dropTriggerSql(query);
    }

    /**
     * Returns SQL string for dropping the trigger.
     * @param query trigger query
     */
    abstract String dropTriggerSql(TriggerQuery query);

    final String tableString(String schemaName, String tableName) {
        return this.dmlAdaptor.tableString(schemaName, tableName);
    }

    final String sequenceString(String schemaName, String sequenceName) {
        return this.dmlAdaptor.sequenceString(schemaName, sequenceName);
    }

    final String pkConstraintString(TableElement tableElement) {
        return this.dmlAdaptor.pkConstraintString(tableElement);
    }

    /**
     * Generates ALTER SEQUENCE script.
     * @param s sequence to alter
     */
    protected List alterSequence(SequenceElement s) {
        String sql = String.format(
                "ALTER SEQUENCE %s %s",
                sequenceString(s.getGrain().getName(), s.getName()),
                generateArgumentsForCreateSequenceExpression(s, SequenceElement.Argument.START_WITH)
        );

        return Collections.singletonList(sql);
    }

    /**
     * Subsitutes arguments for CREATE SEQUENCE expression.
     * @param s sequence
     * @param excludedArguments arguments to exclude
     */
    String generateArgumentsForCreateSequenceExpression(
            SequenceElement s, SequenceElement.Argument... excludedArguments) {

        return s.getArguments().entrySet().stream()
                .filter(e -> !Arrays.asList(excludedArguments).contains(e.getKey()))
                .map(
                        e -> e.getKey().getSql(e.getValue())
                ).collect(Collectors.joining());
    }

    /**
     * Returns String representation of table definition for RDBMS.
     *
     * @param te TableElement metadata provided by Celesta.
     * @return Returns String representation of table definition for RDBMS.
     */
    String createTable(TableElement te) {
        StringBuilder sb = new StringBuilder();
        // Table definition with columns
        sb.append(
                "create table " + tableString(te.getGrain().getName(), te.getName()) + "(\n"
        );
        boolean multiple = false;
        for (Column c : te.getColumns().values()) {
            if (multiple) {
                sb.append(",\n");
            }
            sb.append("  " + columnDef(c));
            multiple = true;
        }

        if (te instanceof VersionedElement) {
            VersionedElement ve = (VersionedElement) te;
            // For versioned tables, the "recversion" column
            if (ve.isVersioned()) {
                sb.append(",\n").append("  " + columnDef(ve.getRecVersionField()));
            }
        }

        if (te.hasPrimeKey()) {
            sb.append(",\n");
            // Primary key definition if it should be present in the table
            sb.append(String.format("  constraint \"%s\" primary key (", pkConstraintString(te)));
            multiple = false;
            for (String s : te.getPrimaryKey().keySet()) {
                if (multiple) {
                    sb.append(", ");
                }
                sb.append('"');
                sb.append(s);
                sb.append('"');
                multiple = true;
            }
            sb.append(")");
        }

        sb.append("\n)");

        return sb.toString();
    }

    /**
     * Generates SQL for dropping primary key from the table by using
     * known name of the primary key.
     *
     * @param t  table table name
     * @param pkName  primary key name
     */
    public abstract String dropPk(TableElement t, String pkName);


    /**
     * Returns String representation of column definition for RDBMS.
     *
     * @param c Column metadata provided by Celesta.
     * @return Returns String representation of column definition for RDBMS.
     */
    //TODO:Must be defined in single place
    final String columnDef(Column c) {
        @SuppressWarnings("unchecked")
        final Class> cClass = (Class>) c.getClass();

        return ColumnDefinerFactory
                .getColumnDefiner(getType(), cClass)
                .getFullDefinition(c);
    }

    final String createColumn(Column c) {
        String sql = String.format(ALTER_TABLE
                + tableString(c.getParentTable().getGrain().getName(), c.getParentTable().getName())
                + " add %s", columnDef(c));
        return sql;
    }

    abstract DBType getType();

    /**
     * Generates SQL for versioning trigger update on a table.
     *
     * @param conn Connection
     * @param t    Table (versioned or unversioned)
     * @throws RuntimeException  on trigger creation or deletion
     */
    abstract List updateVersioningTrigger(Connection conn, TableElement t);

    abstract List createIndex(Index index);

    /**
     * Alters a table column.
     *
     * @param c  column for update
     * @throws RuntimeException  on column update error
     */
    abstract List updateColumn(Connection conn, Column c, DbColumnInfo actual);

    final String createPk(TableElement t) {
        StringBuilder sb = new StringBuilder();

        sb.append(
                String.format(
                        "alter table %s add constraint \"%s\" " + " primary key (",
                        tableString(t.getGrain().getName(), t.getName()), pkConstraintString(t)
                )
        );

        boolean multiple = false;
        for (String s : t.getPrimaryKey().keySet()) {
            if (multiple) {
                sb.append(", ");
            }
            sb.append('"');
            sb.append(s);
            sb.append('"');
            multiple = true;
        }
        sb.append(")");

        return sb.toString();
    }

    final List createFk(Connection conn, ForeignKey fk)  {
        Deque sqlQueue = new LinkedList<>();

        // Building a query for FK creation
        StringBuilder sql = new StringBuilder();
        sql.append(ALTER_TABLE);
        sql.append(tableString(fk.getParentTable().getGrain().getName(), fk.getParentTable().getName()));
        sql.append(" add constraint \"");
        sql.append(fk.getConstraintName());
        sql.append("\" foreign key (");
        boolean needComma = false;
        for (String name : fk.getColumns().keySet()) {
            if (needComma) {
                sql.append(", ");
            }
            sql.append('"');
            sql.append(name);
            sql.append('"');
            needComma = true;
        }
        sql.append(") references ");
        sql.append(tableString(fk.getReferencedTable().getGrain().getName(),
                fk.getReferencedTable().getName()));
        sql.append("(");
        needComma = false;
        for (String name : fk.getReferencedTable().getPrimaryKey().keySet()) {
            if (needComma) {
                sql.append(", ");
            }
            sql.append('"');
            sql.append(name);
            sql.append('"');
            needComma = true;
        }
        sql.append(")");

        switch (fk.getDeleteRule()) {
            case SET_NULL:
                sql.append(" on delete set null");
                break;
            case CASCADE:
                sql.append(" on delete cascade");
                break;
            case NO_ACTION:
            default:
                break;
        }

        sqlQueue.add(sql);
        processCreateUpdateRule(conn, fk, sqlQueue);

        return sqlQueue.stream()
                .map(StringBuilder::toString)
                .collect(Collectors.toList());
    }

    /**
     * Add on update rules to sql syntax.
     * @param conn connection
     * @param fk foreign key
     * @param sqlQueue queue of queries
     */
    void processCreateUpdateRule(Connection conn, ForeignKey fk, Deque sqlQueue)  {
        StringBuilder sql = sqlQueue.peek();
        switch (fk.getUpdateRule()) {
            case SET_NULL:
                sql.append(" on update set null");
                break;
            case CASCADE:
                sql.append(" on update cascade");
                break;
            case NO_ACTION:
            default:
                break;
        }
    }

    final String createView(View v)  {
        try {
            SQLGenerator gen = getViewSQLGenerator();
            StringWriter sw = new StringWriter();
            PrintWriter bw = new PrintWriter(sw);

            v.createViewScript(bw, gen);
            bw.flush();

            String sql = sw.toString();
            return sql;
        } catch (IOException e) {
            throw new CelestaException(e);
        }
    }

    /**
     * This method is called after a table creation.
     *
     * @param t  table
     * @return  list of SQLs to be processed after a table creation
     */
    List afterCreateTable(Connection conn, TableElement t) {
        return Collections.emptyList();
    }

    final String dropTable(TableElement t) {
        String sql = String.format(
                "DROP TABLE %s",
                tableString(t.getGrain().getName(), t.getName())
        );

        this.triggers.computeIfAbsent(t.getGrain().getName(), s -> new HashMap<>())
                .remove(t.getName());

        return sql;
    }

    final List initDataForMaterializedView(MaterializedView mv) {
        TableElement t = mv.getRefTable().getTable();

        String mvIdentifier = tableString(mv.getGrain().getName(), mv.getName());
        String mvColumns = mv.getColumns().keySet().stream()
                .filter(alias -> !MaterializedView.SURROGATE_COUNT.equals(alias))
                .map(alias -> "\"" + alias + "\"")
                .collect(Collectors.joining(", "))
                .concat(", \"").concat(MaterializedView.SURROGATE_COUNT).concat("\"");

        String tableGroupByColumns = mv.getColumns().values().stream()
                .filter(v -> mv.isGroupByColumn(v.getName()))
                .map(v -> {
                    Column colRef = mv.getColumnRef(v.getName());
                    String groupByColStr = "\"" + mv.getColumnRef(v.getName()).getName() + "\"";

                    if (DateTimeColumn.CELESTA_TYPE.equals(colRef.getCelestaType())) {
                        return truncDate(groupByColStr);
                    }
                    return groupByColStr;
                })
                .collect(Collectors.joining(", "));

        String deleteSql = this.truncateTable(mvIdentifier);

        String colsToSelect = mv.getColumns().keySet().stream()
                .filter(alias -> !MaterializedView.SURROGATE_COUNT.equals(alias))
                .map(alias -> {
                    Column colRef = mv.getColumnRef(alias);
                    Map aggrCols = mv.getAggregateColumns();

                    if (aggrCols.containsKey(alias)) {
                        Expr agrExpr = aggrCols.get(alias);
                        if (agrExpr instanceof Count) {
                            return "COUNT(*)";
                        } else if (agrExpr instanceof Sum) {
                            return "SUM(\"" + colRef.getName() + "\")";
                        } else {
                            throw new RuntimeException(
                                    String.format(
                                            "Aggregate func of type %s is not supported",
                                            agrExpr.getClass().getSimpleName()
                                    )
                            );
                        }
                    } else {
                        if (DateTimeColumn.CELESTA_TYPE.equals(colRef.getCelestaType())) {
                            return truncDate("\"" + colRef.getName() + "\"");
                        }
                        return "\"" + colRef.getName() + "\"";

                    }
                }).collect(Collectors.joining(", "));

        String selectScript = String.format("SELECT " + colsToSelect + ", COUNT(*)"
                        + " FROM " + tableString(t.getGrain().getName(), t.getName()) + " GROUP BY %s",
                tableGroupByColumns);
        String insertSql = String.format("INSERT INTO %s (%s) " + selectScript, mvIdentifier, mvColumns);

        return Arrays.asList(deleteSql, insertSql);
    }

    /**
     * Generates TRUNCATE TABLE script.
     * @param tableName name of the table to truncate
     */
    String truncateTable(String tableName) {
        return "TRUNCATE TABLE " + tableName;
    }

    final boolean triggerExists(Connection conn, TriggerQuery query)  {
        try {
            return isTriggerKnown(query) || this.dmlAdaptor.triggerExists(conn, query);
        } catch (SQLException e) {
            throw new CelestaException(e);
        }
    }

    final void rememberTrigger(TriggerQuery query) {
        this.triggers.computeIfAbsent(query.getSchema(), s -> new HashMap<>())
                .computeIfAbsent(query.getTableName(), t -> new HashSet<>())
                .add(query.getName());
    }

    final void forgetTrigger(TriggerQuery query) {
        this.triggers.computeIfAbsent(query.getSchema(), s -> new HashMap<>())
                .computeIfAbsent(query.getTableName(), t -> new HashSet<>())
                .remove(query.getName());
    }

    final boolean isTriggerKnown(TriggerQuery query) {
        return this.triggers.computeIfAbsent(query.getSchema(), s -> new HashMap<>())
                .computeIfAbsent(query.getTableName(), t -> new HashSet<>())
                .contains(query.getName());
    }

    /**
     * Returns a translator from CelestaSQL language to the language of desired DB dialect.
     *
     */
    abstract SQLGenerator getViewSQLGenerator();

    abstract List createParameterizedView(ParameterizedView pv);

    abstract Optional dropAutoIncrement(Connection conn, TableElement t);

    public abstract List dropTableTriggersForMaterializedViews(Connection conn, BasicTable t);

    public abstract List createTableTriggersForMaterializedViews(BasicTable t);

    /**
     * Returns an SQL with the rounding function of timestamp to date.
     *
     * @param dateStr  value that has to be rounded
     */
    abstract String truncDate(String dateStr);
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy