/*
 * Copyright 2004-2019 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (http://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.command.ddl;

import java.util.ArrayList;
import java.util.HashSet;

import org.h2.api.ErrorCode;
import org.h2.command.CommandInterface;
import org.h2.command.Parser;
import org.h2.command.Prepared;
import org.h2.constraint.Constraint;
import org.h2.constraint.ConstraintReferential;
import org.h2.constraint.ConstraintUnique;
import org.h2.engine.Constants;
import org.h2.engine.Database;
import org.h2.engine.DbObject;
import org.h2.engine.Right;
import org.h2.engine.Session;
import org.h2.expression.Expression;
import org.h2.expression.ExpressionVisitor;
import org.h2.index.Index;
import org.h2.index.IndexType;
import org.h2.message.DbException;
import org.h2.result.ResultInterface;
import org.h2.result.SearchRow;
import org.h2.schema.Schema;
import org.h2.schema.SchemaObject;
import org.h2.schema.Sequence;
import org.h2.schema.TriggerObject;
import org.h2.table.Column;
import org.h2.table.Table;
import org.h2.table.TableBase;
import org.h2.table.TableView;
import org.h2.util.Utils;

This class represents the statements ALTER TABLE ADD, ALTER TABLE ADD IF NOT EXISTS, ALTER TABLE ALTER COLUMN, ALTER TABLE ALTER COLUMN RESTART, ALTER TABLE ALTER COLUMN SELECTIVITY, ALTER TABLE ALTER COLUMN SET DEFAULT, ALTER TABLE ALTER COLUMN SET NOT NULL, ALTER TABLE ALTER COLUMN SET NULL, ALTER TABLE ALTER COLUMN SET VISIBLE, ALTER TABLE ALTER COLUMN SET INVISIBLE, ALTER TABLE DROP COLUMN
/** * This class represents the statements * ALTER TABLE ADD, * ALTER TABLE ADD IF NOT EXISTS, * ALTER TABLE ALTER COLUMN, * ALTER TABLE ALTER COLUMN RESTART, * ALTER TABLE ALTER COLUMN SELECTIVITY, * ALTER TABLE ALTER COLUMN SET DEFAULT, * ALTER TABLE ALTER COLUMN SET NOT NULL, * ALTER TABLE ALTER COLUMN SET NULL, * ALTER TABLE ALTER COLUMN SET VISIBLE, * ALTER TABLE ALTER COLUMN SET INVISIBLE, * ALTER TABLE DROP COLUMN */
public class AlterTableAlterColumn extends CommandWithColumns { private String tableName; private Column oldColumn; private Column newColumn; private int type;
Default or on update expression.
/** * Default or on update expression. */
private Expression defaultExpression; private Expression newSelectivity; private boolean addFirst; private String addBefore; private String addAfter; private boolean ifTableExists; private boolean ifNotExists; private ArrayList<Column> columnsToAdd; private ArrayList<Column> columnsToRemove; private boolean newVisibility; public AlterTableAlterColumn(Session session, Schema schema) { super(session, schema); } public void setIfTableExists(boolean b) { ifTableExists = b; } public void setTableName(String tableName) { this.tableName = tableName; } public void setOldColumn(Column oldColumn) { this.oldColumn = oldColumn; }
Add the column as the first column of the table.
/** * Add the column as the first column of the table. */
public void setAddFirst() { addFirst = true; } public void setAddBefore(String before) { this.addBefore = before; } public void setAddAfter(String after) { this.addAfter = after; } @Override public int update() { session.commit(true); Database db = session.getDatabase(); Table table = getSchema().resolveTableOrView(session, tableName); if (table == null) { if (ifTableExists) { return 0; } throw DbException.get(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1, tableName); } session.getUser().checkRight(table, Right.ALL); table.checkSupportAlter(); table.lock(session, true, true); if (newColumn != null) { checkDefaultReferencesTable(table, newColumn.getDefaultExpression()); checkClustering(newColumn); } if (columnsToAdd != null) { for (Column column : columnsToAdd) { checkDefaultReferencesTable(table, column.getDefaultExpression()); checkClustering(column); } } switch (type) { case CommandInterface.ALTER_TABLE_ALTER_COLUMN_NOT_NULL: { if (!oldColumn.isNullable()) { // no change break; } checkNoNullValues(table); oldColumn.setNullable(false); db.updateMeta(session, table); break; } case CommandInterface.ALTER_TABLE_ALTER_COLUMN_DROP_NOT_NULL: { if (oldColumn.isNullable()) { // no change break; } checkNullable(table); oldColumn.setNullable(true); db.updateMeta(session, table); break; } case CommandInterface.ALTER_TABLE_ALTER_COLUMN_DEFAULT: { Sequence sequence = oldColumn == null ? null : oldColumn.getSequence(); checkDefaultReferencesTable(table, defaultExpression); oldColumn.setSequence(null); oldColumn.setDefaultExpression(session, defaultExpression); removeSequence(table, sequence); db.updateMeta(session, table); break; } case CommandInterface.ALTER_TABLE_ALTER_COLUMN_ON_UPDATE: { checkDefaultReferencesTable(table, defaultExpression); oldColumn.setOnUpdateExpression(session, defaultExpression); db.updateMeta(session, table); break; } case CommandInterface.ALTER_TABLE_ALTER_COLUMN_CHANGE_TYPE: { // if the change is only increasing the precision, then we don't // need to copy the table because the length is only a constraint, // and does not affect the storage structure. if (oldColumn.isWideningConversion(newColumn)) { convertAutoIncrementColumn(table, newColumn); oldColumn.copy(newColumn); db.updateMeta(session, table); } else { oldColumn.setSequence(null); oldColumn.setDefaultExpression(session, null); oldColumn.setConvertNullToDefault(false); if (oldColumn.isNullable() && !newColumn.isNullable()) { checkNoNullValues(table); } else if (!oldColumn.isNullable() && newColumn.isNullable()) { checkNullable(table); } if (oldColumn.getVisible() ^ newColumn.getVisible()) { oldColumn.setVisible(newColumn.getVisible()); } convertAutoIncrementColumn(table, newColumn); copyData(table); } table.setModified(); break; } case CommandInterface.ALTER_TABLE_ADD_COLUMN: { // ifNotExists only supported for single column add if (ifNotExists && columnsToAdd != null && columnsToAdd.size() == 1 && table.doesColumnExist(columnsToAdd.get(0).getName())) { break; } ArrayList<Sequence> sequences = generateSequences(columnsToAdd, false); if (columnsToAdd != null) { changePrimaryKeysToNotNull(columnsToAdd); } copyData(table, sequences, true); break; } case CommandInterface.ALTER_TABLE_DROP_COLUMN: { if (table.getColumns().length - columnsToRemove.size() < 1) { throw DbException.get(ErrorCode.CANNOT_DROP_LAST_COLUMN, columnsToRemove.get(0).getSQL(false)); } table.dropMultipleColumnsConstraintsAndIndexes(session, columnsToRemove); copyData(table); break; } case CommandInterface.ALTER_TABLE_ALTER_COLUMN_SELECTIVITY: { int value = newSelectivity.optimize(session).getValue(session).getInt(); oldColumn.setSelectivity(value); db.updateMeta(session, table); break; } case CommandInterface.ALTER_TABLE_ALTER_COLUMN_VISIBILITY: { oldColumn.setVisible(newVisibility); table.setModified(); db.updateMeta(session, table); break; } default: DbException.throwInternalError("type=" + type); } return 0; } private static void checkDefaultReferencesTable(Table table, Expression defaultExpression) { if (defaultExpression == null) { return; } HashSet<DbObject> dependencies = new HashSet<>(); ExpressionVisitor visitor = ExpressionVisitor .getDependenciesVisitor(dependencies); defaultExpression.isEverything(visitor); if (dependencies.contains(table)) { throw DbException.get(ErrorCode.COLUMN_IS_REFERENCED_1, defaultExpression.getSQL(false)); } } private void checkClustering(Column c) { if (!Constants.CLUSTERING_DISABLED .equals(session.getDatabase().getCluster()) && c.isAutoIncrement()) { throw DbException.getUnsupportedException( "CLUSTERING && auto-increment columns"); } } private void convertAutoIncrementColumn(Table table, Column c) { if (c.isAutoIncrement()) { if (c.isPrimaryKey()) { c.setOriginalSQL("IDENTITY"); } else { int objId = getObjectId(); c.convertAutoIncrementToSequence(session, getSchema(), objId, table.isTemporary()); } } } private void removeSequence(Table table, Sequence sequence) { if (sequence != null) { table.removeSequence(sequence); sequence.setBelongsToTable(false); Database db = session.getDatabase(); db.removeSchemaObject(session, sequence); } } private void copyData(Table table) { copyData(table, null, false); } private void copyData(Table table, ArrayList<Sequence> sequences, boolean createConstraints) { if (table.isTemporary()) { throw DbException.getUnsupportedException("TEMP TABLE"); } Database db = session.getDatabase(); String baseName = table.getName(); String tempName = db.getTempTableName(baseName, session); Column[] columns = table.getColumns(); ArrayList<Column> newColumns = new ArrayList<>(columns.length); Table newTable = cloneTableStructure(table, columns, db, tempName, newColumns); if (sequences != null) { for (Sequence sequence : sequences) { table.addSequence(sequence); } } try { // check if a view would become invalid // (because the column to drop is referenced or so) checkViews(table, newTable); } catch (DbException e) { StringBuilder builder = new StringBuilder("DROP TABLE "); newTable.getSQL(builder, true); execute(builder.toString(), true); throw e; } String tableName = table.getName(); ArrayList<TableView> dependentViews = new ArrayList<>(table.getDependentViews()); for (TableView view : dependentViews) { table.removeDependentView(view); } StringBuilder builder = new StringBuilder("DROP TABLE "); table.getSQL(builder, true).append(" IGNORE"); execute(builder.toString(), true); db.renameSchemaObject(session, newTable, tableName); for (DbObject child : newTable.getChildren()) { if (child instanceof Sequence) { continue; } String name = child.getName(); if (name == null || child.getCreateSQL() == null) { continue; } if (name.startsWith(tempName + "_")) { name = name.substring(tempName.length() + 1); SchemaObject so = (SchemaObject) child; if (so instanceof Constraint) { if (so.getSchema().findConstraint(session, name) != null) { name = so.getSchema().getUniqueConstraintName(session, newTable); } } else if (so instanceof Index) { if (so.getSchema().findIndex(session, name) != null) { name = so.getSchema().getUniqueIndexName(session, newTable, name); } } db.renameSchemaObject(session, so, name); } } if (createConstraints) { createConstraints(); } for (TableView view : dependentViews) { String sql = view.getCreateSQL(true, true); execute(sql, true); } } private Table cloneTableStructure(Table table, Column[] columns, Database db, String tempName, ArrayList<Column> newColumns) { for (Column col : columns) { newColumns.add(col.getClone()); } if (type == CommandInterface.ALTER_TABLE_DROP_COLUMN) { for (Column removeCol : columnsToRemove) { Column foundCol = null; for (Column newCol : newColumns) { if (newCol.getName().equals(removeCol.getName())) { foundCol = newCol; break; } } if (foundCol == null) { throw DbException.throwInternalError(removeCol.getCreateSQL()); } newColumns.remove(foundCol); } } else if (type == CommandInterface.ALTER_TABLE_ADD_COLUMN) { int position; if (addFirst) { position = 0; } else if (addBefore != null) { position = table.getColumn(addBefore).getColumnId(); } else if (addAfter != null) { position = table.getColumn(addAfter).getColumnId() + 1; } else { position = columns.length; } if (columnsToAdd != null) { for (Column column : columnsToAdd) { newColumns.add(position++, column); } } } else if (type == CommandInterface.ALTER_TABLE_ALTER_COLUMN_CHANGE_TYPE) { int position = oldColumn.getColumnId(); newColumns.set(position, newColumn); } // create a table object in order to get the SQL statement // can't just use this table, because most column objects are 'shared' // with the old table // still need a new id because using 0 would mean: the new table tries // to use the rows of the table 0 (the meta table) int id = db.allocateObjectId(); CreateTableData data = new CreateTableData(); data.tableName = tempName; data.id = id; data.columns = newColumns; data.temporary = table.isTemporary(); data.persistData = table.isPersistData(); data.persistIndexes = table.isPersistIndexes(); data.isHidden = table.isHidden(); data.create = true; data.session = session; Table newTable = getSchema().createTable(data); newTable.setComment(table.getComment()); String newTableSQL = newTable.getCreateSQL(); StringBuilder columnList = new StringBuilder(); for (Column nc : newColumns) { if (columnList.length() > 0) { columnList.append(", "); } if (type == CommandInterface.ALTER_TABLE_ADD_COLUMN && columnsToAdd != null && columnsToAdd.contains(nc)) { Expression def = nc.getDefaultExpression(); if (def == null) { columnList.append("NULL"); } else { def.getSQL(columnList, true); } } else { nc.getSQL(columnList, true); } } String newTableName = newTable.getName(); Schema newTableSchema = newTable.getSchema(); newTable.removeChildrenAndResources(session); execute(newTableSQL, true); newTable = newTableSchema.getTableOrView(session, newTableName); ArrayList<String> children = Utils.newSmallArrayList(); ArrayList<String> triggers = Utils.newSmallArrayList(); boolean hasDelegateIndex = false; for (DbObject child : table.getChildren()) { if (child instanceof Sequence) { continue; } else if (child instanceof Index) { Index idx = (Index) child; if (idx.getIndexType().getBelongsToConstraint()) { continue; } } String createSQL = child.getCreateSQL(); if (createSQL == null) { continue; } if (child instanceof TableView) { continue; } else if (child.getType() == DbObject.TABLE_OR_VIEW) { DbException.throwInternalError(); } String quotedName = Parser.quoteIdentifier(tempName + "_" + child.getName(), true); String sql = null; if (child instanceof ConstraintReferential) { ConstraintReferential r = (ConstraintReferential) child; if (r.getTable() != table) { sql = r.getCreateSQLForCopy(r.getTable(), newTable, quotedName, false); } } if (sql == null) { sql = child.getCreateSQLForCopy(newTable, quotedName); } if (sql != null) { if (child instanceof TriggerObject) { triggers.add(sql); } else { if (!hasDelegateIndex) { Index index = null; if (child instanceof ConstraintUnique) { ConstraintUnique constraint = (ConstraintUnique) child; if (constraint.getConstraintType() == Constraint.Type.PRIMARY_KEY) { index = constraint.getUniqueIndex(); } } else if (child instanceof Index) { index = (Index) child; } if (index != null && TableBase.getMainIndexColumn(index.getIndexType(), index.getIndexColumns()) != SearchRow.ROWID_INDEX) { execute(sql, true); hasDelegateIndex = true; continue; } } children.add(sql); } } } StringBuilder buff = new StringBuilder(); buff.append("INSERT INTO "); newTable.getSQL(buff, true); buff.append(" SELECT "); if (columnList.length() == 0) { // special case: insert into test select * from buff.append('*'); } else { buff.append(columnList); } buff.append(" FROM "); table.getSQL(buff, true); try { execute(buff.toString(), true); } catch (Throwable t) { // data was not inserted due to data conversion error or some // unexpected reason StringBuilder builder = new StringBuilder("DROP TABLE "); newTable.getSQL(builder, true); execute(builder.toString(), true); throw t; } for (String sql : children) { execute(sql, true); } table.setModified(); // remove the sequences from the columns (except dropped columns) // otherwise the sequence is dropped if the table is dropped for (Column col : newColumns) { Sequence seq = col.getSequence(); if (seq != null) { table.removeSequence(seq); col.setSequence(null); } } for (String sql : triggers) { execute(sql, true); } return newTable; }
Check that all views and other dependent objects.
/** * Check that all views and other dependent objects. */
private void checkViews(SchemaObject sourceTable, SchemaObject newTable) { String sourceTableName = sourceTable.getName(); String newTableName = newTable.getName(); Database db = sourceTable.getDatabase(); // save the real table under a temporary name String temp = db.getTempTableName(sourceTableName, session); db.renameSchemaObject(session, sourceTable, temp); try { // have our new table impersonate the target table db.renameSchemaObject(session, newTable, sourceTableName); checkViewsAreValid(sourceTable); } finally { // always put the source tables back with their proper names try { db.renameSchemaObject(session, newTable, newTableName); } finally { db.renameSchemaObject(session, sourceTable, sourceTableName); } } }
Check that a table or view is still valid.
Params:
  • tableOrView – the table or view to check
/** * Check that a table or view is still valid. * * @param tableOrView the table or view to check */
private void checkViewsAreValid(DbObject tableOrView) { for (DbObject view : tableOrView.getChildren()) { if (view instanceof TableView) { String sql = ((TableView) view).getQuery(); // check if the query is still valid // do not execute, not even with limit 1, because that could // have side effects or take a very long time try { session.prepare(sql); } catch (DbException e) { throw DbException.get(ErrorCode.COLUMN_IS_REFERENCED_1, e, view.getSQL(false)); } checkViewsAreValid(view); } } } private void execute(String sql, boolean ddl) { Prepared command = session.prepare(sql); command.update(); if (ddl) { session.commit(true); } } private void checkNullable(Table table) { for (Index index : table.getIndexes()) { if (index.getColumnIndex(oldColumn) < 0) { continue; } IndexType indexType = index.getIndexType(); if (indexType.isPrimaryKey() || indexType.isHash()) { throw DbException.get(ErrorCode.COLUMN_IS_PART_OF_INDEX_1, index.getSQL(false)); } } } private void checkNoNullValues(Table table) { StringBuilder builder = new StringBuilder("SELECT COUNT(*) FROM "); table.getSQL(builder, true).append(" WHERE "); oldColumn.getSQL(builder, true).append(" IS NULL"); String sql = builder.toString(); Prepared command = session.prepare(sql); ResultInterface result = command.query(0); result.next(); if (result.currentRow()[0].getInt() > 0) { throw DbException.get(ErrorCode.COLUMN_CONTAINS_NULL_VALUES_1, oldColumn.getSQL(false)); } } public void setType(int type) { this.type = type; } public void setSelectivity(Expression selectivity) { newSelectivity = selectivity; }
Set default or on update expression.
Params:
  • defaultExpression – default or on update expression
/** * Set default or on update expression. * * @param defaultExpression default or on update expression */
public void setDefaultExpression(Expression defaultExpression) { this.defaultExpression = defaultExpression; } public void setNewColumn(Column newColumn) { this.newColumn = newColumn; } @Override public int getType() { return type; } public void setIfNotExists(boolean ifNotExists) { this.ifNotExists = ifNotExists; } @Override public void addColumn(Column column) { if (columnsToAdd == null) { columnsToAdd = new ArrayList<>(); } columnsToAdd.add(column); } public void setColumnsToRemove(ArrayList<Column> columnsToRemove) { this.columnsToRemove = columnsToRemove; } public void setVisible(boolean visible) { this.newVisibility = visible; } }