package org.krakenapps.sqlengine.bdb; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.krakenapps.sqlengine.DatabaseHandle; import org.krakenapps.sqlengine.RowKey; import org.krakenapps.sqlengine.RowValue; import org.krakenapps.sqlengine.TableCursor; import org.krakenapps.sqlengine.Status; import org.krakenapps.sqlengine.TableHandle; import org.krakenapps.sqlengine.TableSchemaManager; import org.krakenapps.sqlparser.ast.AddColumnDefinition; import org.krakenapps.sqlparser.ast.AlterTableAction; import org.krakenapps.sqlparser.ast.AlterTableStatement; import org.krakenapps.sqlparser.ast.ColumnConstraintDefinition; import org.krakenapps.sqlparser.ast.ColumnDefinition; import org.krakenapps.sqlparser.ast.DataType; import org.krakenapps.sqlparser.ast.DropColumnDefinition; import org.krakenapps.sqlparser.ast.NotNullConstraint; import org.krakenapps.sqlparser.ast.ReferencesSpecification; import org.krakenapps.sqlparser.ast.TableDefinition; public class TableSchemaManagerImpl implements TableSchemaManager { private TableHandle tableHandle; public TableSchemaManagerImpl(DatabaseHandle databaseHandle) { tableHandle = databaseHandle.openTable("_schema", true); } @Override public TableDefinition getTableSchema(String tableName) throws SQLException { TableCursor cursor = tableHandle.openCursor(); try { RowKey rowKey = new RowKey(tableName); RowValue rowValue = new RowValue(); Status status = tableHandle.get(rowKey, rowValue); if (status != Status.Success) return null; List<ColumnDefinition> columnDefinitions = deserialize(rowValue.getData()); TableDefinition def = new TableDefinition(tableName, columnDefinitions); return def; } finally { cursor.close(); } } @Override public void createTable(TableDefinition definition) throws SQLException { RowValue rowValue = getRowValue(definition); tableHandle.insert(new RowKey(definition.getTableName()), rowValue); } private RowValue getRowValue(TableDefinition definition) { Object[] columns = serialize(definition.getColumnDefinitions()); RowValue rowValue = new RowValue(columns); return rowValue; } @Override public void alterTable(AlterTableStatement stmt) throws SQLException { TableDefinition tableDefinition = getTableSchema(stmt.getTableName()); if (tableDefinition == null) throw new SQLException("table [" + stmt.getTableName() + "] not found"); AlterTableAction action = stmt.getAction(); if (action instanceof AddColumnDefinition) { String tableName = tableDefinition.getTableName(); ColumnDefinition newColumnDefinition = ((AddColumnDefinition) action).getColumnDefinition(); // not null without default value is not allowed for (ColumnConstraintDefinition ccd : newColumnDefinition.getConstraints()) { if (ccd.getColumnConstraint() instanceof NotNullConstraint) // TODO: check also default value throw new SQLException("cannot add not null column without default value."); } // update table schema tableDefinition.getColumnDefinitions().add(newColumnDefinition); tableHandle.update(new RowKey(tableName), getRowValue(tableDefinition)); } else if (action instanceof DropColumnDefinition) { String tableName = tableDefinition.getTableName(); DropColumnDefinition dropAction = (DropColumnDefinition) action; String dropColumnName = dropAction.getColumnName(); // check if target column exists ColumnDefinition dropColumnDefinition = tableDefinition.findColumnDefinition(dropColumnName); if (dropColumnDefinition == null) throw new SQLException("column [" + dropColumnName + "] does not exist"); // check if it is last column if (tableDefinition.getColumnDefinitions().size() == 1) throw new SQLException("cannot drop all columns of table [" + tableName + "]"); // check if it is referenced column checkIfReferencedTable(tableName, dropColumnName); // update table definition tableDefinition.getColumnDefinitions().remove(dropColumnDefinition); RowValue rowValue = getRowValue(tableDefinition); tableHandle.update(new RowKey(tableName), rowValue); } } private void checkIfReferencedTable(String refTableName) throws SQLException { TableDefinition refSchema = getTableSchema(refTableName); for (ColumnDefinition cd : refSchema.getColumnDefinitions()) { checkIfReferencedTable(refTableName, cd.getColumnName()); } } private void checkIfReferencedTable(String refTableName, String refColumnName) throws SQLException { for (String tableName : getTableNames()) { TableDefinition tableDefinition = getTableSchema(tableName); if (isReferencedColumn(tableDefinition, refTableName, refColumnName)) throw new SQLException(String.format("\"%s\" column of \"%s\" table is referenced by \"%s\" table.", refColumnName, refTableName, tableDefinition.getTableName())); } } private boolean isReferencedColumn(TableDefinition tableDefinition, String refTableName, String refColumnName) { for (ColumnDefinition cd : tableDefinition.getColumnDefinitions()) { for (ColumnConstraintDefinition ccd : cd.getConstraints()) { if (!(ccd.getColumnConstraint() instanceof ReferencesSpecification)) continue; ReferencesSpecification rs = (ReferencesSpecification) ccd.getColumnConstraint(); if (rs.getTableName().equals(refTableName) && rs.getColumns().contains(refColumnName)) return true; } } return false; } @Override public void dropTable(String tableName) throws SQLException { // check if it is referenced table checkIfReferencedTable(tableName); tableHandle.delete(new RowKey(tableName)); } @Override public Collection<String> getTableNames() { List<String> s = new ArrayList<String>(); TableCursor cursor = tableHandle.openCursor(); try { RowKey rowKey = new RowKey(); RowValue rowValue = new RowValue(); while (true) { Status status = cursor.getNext(rowKey, rowValue); if (status != Status.Success) break; s.add((String) rowKey.get()); } return s; } finally { cursor.close(); } } private Object[] serialize(List<ColumnDefinition> defs) { Object[] columns = new Object[defs.size()]; int i = 0; for (ColumnDefinition def : defs) { Map<String, Object> m = new HashMap<String, Object>(); m.put("name", def.getColumnName()); m.put("type", def.getDataType().toString()); m.put("constraints", ConstraintSerializer.serialize(def.getConstraints())); columns[i++] = m; } return columns; } @SuppressWarnings("unchecked") private List<ColumnDefinition> deserialize(Object[] columns) { List<ColumnDefinition> defs = new ArrayList<ColumnDefinition>(); for (Object column : columns) { Map<String, Object> m = (Map<String, Object>) column; String columnName = (String) m.get("name"); DataType dataType = DataTypeSerializer.deserialize((String) m.get("type")); List<ColumnConstraintDefinition> constraints = ConstraintSerializer.deserialize((Object[]) m.get("constraints")); defs.add(new ColumnDefinition(columnName, dataType, constraints)); } return defs; } }