diff --git a/bundles/org.openhab.persistence.jdbc/README.md b/bundles/org.openhab.persistence.jdbc/README.md index 0623328a385..fbb4ac3d106 100644 --- a/bundles/org.openhab.persistence.jdbc/README.md +++ b/bundles/org.openhab.persistence.jdbc/README.md @@ -208,6 +208,12 @@ Manual changes in the index table, `Items`, will not be picked up automatically The same is true when manually adding new item tables or deleting existing ones. After making such changes, the command `jdbc reload` can be used to reload the index. +#### Check/fix Schema + +Use the command `jdbc schema check` to perform an integrity check of the schema. + +Identified issues can be fixed automatically using the command `jdbc schema fix` (all items having issues) or `jdbc schema fix ` (single item). + ### For Developers * Clearly separated source files for the database-specific part of openHAB logic. diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java index f68220d5e8c..1b3c1652262 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java @@ -31,6 +31,7 @@ import org.openhab.core.persistence.FilterCriteria; import org.openhab.core.persistence.HistoricItem; import org.openhab.core.persistence.PersistenceItemInfo; import org.openhab.core.types.State; +import org.openhab.persistence.jdbc.internal.dto.Column; import org.openhab.persistence.jdbc.internal.dto.ItemVO; import org.openhab.persistence.jdbc.internal.dto.ItemsVO; import org.openhab.persistence.jdbc.internal.dto.JdbcPersistenceItemInfo; @@ -171,6 +172,17 @@ public class JdbcMapper { return vol; } + protected List getTableColumns(String tableName) throws JdbcSQLException { + logger.debug("JDBC::getTableColumns"); + long timerStart = System.currentTimeMillis(); + ItemsVO isvo = new ItemsVO(); + isvo.setJdbcUriDatabaseName(conf.getDbName()); + isvo.setTableName(tableName); + List is = conf.getDBDAO().doGetTableColumns(isvo); + logTime("getTableColumns", timerStart, System.currentTimeMillis()); + return is; + } + /**************** * MAPPERS ITEM * ****************/ @@ -189,6 +201,14 @@ public class JdbcMapper { return vo; } + protected void alterTableColumn(String tableName, String columnName, String columnType, boolean nullable) + throws JdbcSQLException { + logger.debug("JDBC::alterTableColumn"); + long timerStart = System.currentTimeMillis(); + conf.getDBDAO().doAlterTableColumn(tableName, columnName, columnType, nullable); + logTime("alterTableColumn", timerStart, System.currentTimeMillis()); + } + protected void storeItemValue(Item item, State itemState, @Nullable ZonedDateTime date) throws JdbcException { logger.debug("JDBC::storeItemValue: item={} state={} date={}", item, itemState, date); String tableName = getTable(item); diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java index a75f6d2c8af..af7da62f953 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java @@ -40,6 +40,8 @@ import org.openhab.core.persistence.QueryablePersistenceService; import org.openhab.core.persistence.strategy.PersistenceStrategy; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; +import org.openhab.persistence.jdbc.internal.db.JdbcBaseDAO; +import org.openhab.persistence.jdbc.internal.dto.Column; import org.openhab.persistence.jdbc.internal.dto.ItemsVO; import org.openhab.persistence.jdbc.internal.exceptions.JdbcException; import org.openhab.persistence.jdbc.internal.exceptions.JdbcSQLException; @@ -303,6 +305,109 @@ public class JdbcPersistenceService extends JdbcMapper implements ModifiablePers return itemNameToTableNameMap.keySet(); } + /** + * Get a map of item names to table names. + */ + public Map getItemNameToTableNameMap() { + return itemNameToTableNameMap; + } + + /** + * Check schema for integrity issues. + * + * @param tableName for which columns should be checked + * @param itemName that corresponds to table + * @return Collection of strings, each describing an identified issue + * @throws JdbcSQLException on SQL errors + */ + public Collection getSchemaIssues(String tableName, String itemName) throws JdbcSQLException { + List issues = new ArrayList<>(); + Item item; + try { + item = itemRegistry.getItem(itemName); + } catch (ItemNotFoundException e) { + return issues; + } + JdbcBaseDAO dao = conf.getDBDAO(); + String timeDataType = dao.sqlTypes.get("tablePrimaryKey"); + if (timeDataType == null) { + return issues; + } + String valueDataType = dao.getDataType(item); + List columns = getTableColumns(tableName); + for (Column column : columns) { + String columnName = column.getColumnName(); + if ("time".equalsIgnoreCase(columnName)) { + if (!"time".equals(columnName)) { + issues.add("Column name 'time' expected, but is '" + columnName + "'"); + } + if (!timeDataType.equalsIgnoreCase(column.getColumnType())) { + issues.add("Column type '" + timeDataType + "' expected, but is '" + + column.getColumnType().toUpperCase() + "'"); + } + if (column.getIsNullable()) { + issues.add("Column 'time' expected to be NOT NULL, but is nullable"); + } + } else if ("value".equalsIgnoreCase(columnName)) { + if (!"value".equals(columnName)) { + issues.add("Column name 'value' expected, but is '" + columnName + "'"); + } + if (!valueDataType.equalsIgnoreCase(column.getColumnType())) { + issues.add("Column type '" + valueDataType + "' expected, but is '" + + column.getColumnType().toUpperCase() + "'"); + } + if (!column.getIsNullable()) { + issues.add("Column 'value' expected to be nullable, but is NOT NULL"); + } + } else { + issues.add("Column '" + columnName + "' not expected"); + } + } + return issues; + } + + /** + * Fix schema issues. + * + * @param tableName for which columns should be repaired + * @param itemName that corresponds to table + * @return true if table was altered, otherwise false + * @throws JdbcSQLException on SQL errors + */ + public boolean fixSchemaIssues(String tableName, String itemName) throws JdbcSQLException { + Item item; + try { + item = itemRegistry.getItem(itemName); + } catch (ItemNotFoundException e) { + return false; + } + JdbcBaseDAO dao = conf.getDBDAO(); + String timeDataType = dao.sqlTypes.get("tablePrimaryKey"); + if (timeDataType == null) { + return false; + } + String valueDataType = dao.getDataType(item); + List columns = getTableColumns(tableName); + boolean isFixed = false; + for (Column column : columns) { + String columnName = column.getColumnName(); + if ("time".equalsIgnoreCase(columnName)) { + if (!"time".equals(columnName) || !timeDataType.equalsIgnoreCase(column.getColumnType()) + || column.getIsNullable()) { + alterTableColumn(tableName, "time", timeDataType, false); + isFixed = true; + } + } else if ("value".equalsIgnoreCase(columnName)) { + if (!"value".equals(columnName) || !valueDataType.equalsIgnoreCase(column.getColumnType()) + || !column.getIsNullable()) { + alterTableColumn(tableName, "value", valueDataType, true); + isFixed = true; + } + } + } + return isFixed; + } + /** * Get a list of all items with corresponding tables and an {@link ItemTableCheckEntryStatus} indicating * its condition. diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/console/JdbcCommandExtension.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/console/JdbcCommandExtension.java index 14f674eef13..2392edba0ab 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/console/JdbcCommandExtension.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/console/JdbcCommandExtension.java @@ -13,8 +13,12 @@ package org.openhab.persistence.jdbc.internal.console; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -44,13 +48,19 @@ import org.osgi.service.component.annotations.Reference; @Component(service = ConsoleCommandExtension.class) public class JdbcCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter { + private static final String CMD_SCHEMA = "schema"; private static final String CMD_TABLES = "tables"; private static final String CMD_RELOAD = "reload"; + private static final String SUBCMD_SCHEMA_CHECK = "check"; + private static final String SUBCMD_SCHEMA_FIX = "fix"; private static final String SUBCMD_TABLES_LIST = "list"; private static final String SUBCMD_TABLES_CLEAN = "clean"; private static final String PARAMETER_ALL = "all"; private static final String PARAMETER_FORCE = "force"; - private static final StringsCompleter CMD_COMPLETER = new StringsCompleter(List.of(CMD_TABLES, CMD_RELOAD), false); + private static final StringsCompleter CMD_COMPLETER = new StringsCompleter( + List.of(CMD_SCHEMA, CMD_TABLES, CMD_RELOAD), false); + private static final StringsCompleter SUBCMD_SCHEMA_COMPLETER = new StringsCompleter( + List.of(SUBCMD_SCHEMA_CHECK, SUBCMD_SCHEMA_FIX), false); private static final StringsCompleter SUBCMD_TABLES_COMPLETER = new StringsCompleter( List.of(SUBCMD_TABLES_LIST, SUBCMD_TABLES_CLEAN), false); @@ -109,6 +119,19 @@ public class JdbcCommandExtension extends AbstractConsoleCommandExtension implem return true; } } + } else if (args.length > 1 && CMD_SCHEMA.equalsIgnoreCase(args[0])) { + if (args.length == 2 && SUBCMD_SCHEMA_CHECK.equalsIgnoreCase(args[1])) { + checkSchema(persistenceService, console); + return true; + } else if (SUBCMD_SCHEMA_FIX.equalsIgnoreCase(args[1])) { + if (args.length == 2) { + fixSchema(persistenceService, console); + return true; + } else if (args.length == 3) { + fixSchema(persistenceService, console, args[2]); + return true; + } + } } else if (args.length == 1 && CMD_RELOAD.equalsIgnoreCase(args[0])) { reload(persistenceService, console); return true; @@ -116,7 +139,62 @@ public class JdbcCommandExtension extends AbstractConsoleCommandExtension implem return false; } - private void listTables(JdbcPersistenceService persistenceService, Console console, Boolean all) + private void checkSchema(JdbcPersistenceService persistenceService, Console console) throws JdbcSQLException { + List> itemNameToTableName = persistenceService.getItemNameToTableNameMap().entrySet() + .stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList()); + int itemNameMaxLength = Math + .max(itemNameToTableName.stream().map(i -> i.getKey().length()).max(Integer::compare).orElse(0), 4); + int tableNameMaxLength = Math + .max(itemNameToTableName.stream().map(i -> i.getValue().length()).max(Integer::compare).orElse(0), 5); + console.println(String.format("%1$-" + (tableNameMaxLength + 2) + "s%2$-" + (itemNameMaxLength + 2) + "s%3$s", + "Table", "Item", "Issue")); + console.println("-".repeat(tableNameMaxLength) + " " + "-".repeat(itemNameMaxLength) + " " + "-".repeat(64)); + for (Entry entry : itemNameToTableName) { + String itemName = entry.getKey(); + String tableName = entry.getValue(); + Collection issues = persistenceService.getSchemaIssues(tableName, itemName); + if (!issues.isEmpty()) { + for (String issue : issues) { + console.println(String.format( + "%1$-" + (tableNameMaxLength + 2) + "s%2$-" + (itemNameMaxLength + 2) + "s%3$s", tableName, + itemName, issue)); + } + } + } + } + + private void fixSchema(JdbcPersistenceService persistenceService, Console console) { + List> itemNameToTableName = persistenceService.getItemNameToTableNameMap().entrySet() + .stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList()); + for (Entry entry : itemNameToTableName) { + String itemName = entry.getKey(); + String tableName = entry.getValue(); + fixSchema(persistenceService, console, tableName, itemName); + } + } + + private void fixSchema(JdbcPersistenceService persistenceService, Console console, String itemName) { + Map itemNameToTableNameMap = persistenceService.getItemNameToTableNameMap(); + String tableName = itemNameToTableNameMap.get(itemName); + if (tableName != null) { + fixSchema(persistenceService, console, tableName, itemName); + } else { + console.println("Table not found for item '" + itemName + "'"); + } + } + + private void fixSchema(JdbcPersistenceService persistenceService, Console console, String tableName, + String itemName) { + try { + if (persistenceService.fixSchemaIssues(tableName, itemName)) { + console.println("Fixed table '" + tableName + "' for item '" + itemName + "'"); + } + } catch (JdbcSQLException e) { + console.println("Failed to fix table '" + tableName + "' for item '" + itemName + "': " + e.getMessage()); + } + } + + private void listTables(JdbcPersistenceService persistenceService, Console console, boolean all) throws JdbcSQLException { List entries = persistenceService.getCheckedEntries(); if (!all) { @@ -176,7 +254,8 @@ public class JdbcCommandExtension extends AbstractConsoleCommandExtension implem @Override public List getUsages() { - return Arrays.asList( + return Arrays.asList(buildCommandUsage(CMD_SCHEMA + " " + SUBCMD_SCHEMA_CHECK, "check schema integrity"), + buildCommandUsage(CMD_SCHEMA + " " + SUBCMD_SCHEMA_FIX + " []", "fix schema integrity"), buildCommandUsage(CMD_TABLES + " " + SUBCMD_TABLES_LIST + " [" + PARAMETER_ALL + "]", "list tables (all = include valid)"), buildCommandUsage( @@ -197,6 +276,8 @@ public class JdbcCommandExtension extends AbstractConsoleCommandExtension implem } else if (cursorArgumentIndex == 1) { if (CMD_TABLES.equalsIgnoreCase(args[0])) { return SUBCMD_TABLES_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates); + } else if (CMD_SCHEMA.equalsIgnoreCase(args[0])) { + return SUBCMD_SCHEMA_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates); } } else if (cursorArgumentIndex == 2) { if (CMD_TABLES.equalsIgnoreCase(args[0])) { @@ -210,6 +291,14 @@ public class JdbcCommandExtension extends AbstractConsoleCommandExtension implem new StringsCompleter(List.of(PARAMETER_ALL), false).complete(args, cursorArgumentIndex, cursorPosition, candidates); } + } else if (CMD_SCHEMA.equalsIgnoreCase(args[0])) { + if (SUBCMD_SCHEMA_FIX.equalsIgnoreCase(args[1])) { + JdbcPersistenceService persistenceService = getPersistenceService(); + if (persistenceService != null) { + return new StringsCompleter(persistenceService.getItemNames(), true).complete(args, + cursorArgumentIndex, cursorPosition, candidates); + } + } } } return false; diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/db/JdbcBaseDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/db/JdbcBaseDAO.java index b0b4aae9ce8..11ec2140feb 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/db/JdbcBaseDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/db/JdbcBaseDAO.java @@ -56,6 +56,7 @@ import org.openhab.core.persistence.FilterCriteria.Ordering; import org.openhab.core.persistence.HistoricItem; import org.openhab.core.types.State; import org.openhab.core.types.TypeParser; +import org.openhab.persistence.jdbc.internal.dto.Column; import org.openhab.persistence.jdbc.internal.dto.ItemVO; import org.openhab.persistence.jdbc.internal.dto.ItemsVO; import org.openhab.persistence.jdbc.internal.dto.JdbcHistoricItem; @@ -91,8 +92,10 @@ public class JdbcBaseDAO { protected String sqlDeleteItemsEntry = "DELETE FROM #itemsManageTable# WHERE ItemName='#itemname#'"; protected String sqlGetItemIDTableNames = "SELECT ItemId, ItemName FROM #itemsManageTable#"; protected String sqlGetItemTables = "SELECT table_name FROM information_schema.tables WHERE table_type='BASE TABLE' AND table_schema='#jdbcUriDatabaseName#' AND NOT table_name='#itemsManageTable#'"; + protected String sqlGetTableColumnTypes = "SELECT column_name, column_type, is_nullable FROM information_schema.columns WHERE table_schema='#jdbcUriDatabaseName#' AND table_name='#tableName#'"; protected String sqlCreateItemTable = "CREATE TABLE IF NOT EXISTS #tableName# (time #tablePrimaryKey# NOT NULL, value #dbType#, PRIMARY KEY(time))"; - protected String sqlInsertItemValue = "INSERT INTO #tableName# (TIME, VALUE) VALUES( #tablePrimaryValue#, ? ) ON DUPLICATE KEY UPDATE VALUE= ?"; + protected String sqlAlterTableColumn = "ALTER TABLE #tableName# MODIFY COLUMN #columnName# #columnType#"; + protected String sqlInsertItemValue = "INSERT INTO #tableName# (time, value) VALUES( #tablePrimaryValue#, ? ) ON DUPLICATE KEY UPDATE VALUE= ?"; protected String sqlGetRowCount = "SELECT COUNT(*) FROM #tableName#"; /******** @@ -375,6 +378,18 @@ public class JdbcBaseDAO { } } + public List doGetTableColumns(ItemsVO vo) throws JdbcSQLException { + String sql = StringUtilsExt.replaceArrayMerge(sqlGetTableColumnTypes, + new String[] { "#jdbcUriDatabaseName#", "#tableName#" }, + new String[] { vo.getJdbcUriDatabaseName(), vo.getTableName() }); + logger.debug("JDBC::doGetTableColumns sql={}", sql); + try { + return Yank.queryBeanList(sql, Column.class, null); + } catch (YankSQLException e) { + throw new JdbcSQLException(e); + } + } + /************* * ITEM DAOs * *************/ @@ -402,6 +417,19 @@ public class JdbcBaseDAO { } } + public void doAlterTableColumn(String tableName, String columnName, String columnType, boolean nullable) + throws JdbcSQLException { + String sql = StringUtilsExt.replaceArrayMerge(sqlAlterTableColumn, + new String[] { "#tableName#", "#columnName#", "#columnType#" }, + new String[] { tableName, columnName, nullable ? columnType : columnType + " NOT NULL" }); + logger.debug("JDBC::doAlterTableColumn sql={}", sql); + try { + Yank.execute(sql, null); + } catch (YankSQLException e) { + throw new JdbcSQLException(e); + } + } + public void doStoreItemValue(Item item, State itemState, ItemVO vo) throws JdbcSQLException { ItemVO storedVO = storeItemValueProvider(item, itemState, vo); String sql = StringUtilsExt.replaceArrayMerge(sqlInsertItemValue, @@ -727,7 +755,6 @@ public class JdbcBaseDAO { } } String itemType = item.getClass().getSimpleName().toUpperCase(); - logger.debug("JDBC::getItemType: Try to use ItemType {} for Item {}", itemType, i.getName()); if (sqlTypes.get(itemType) == null) { logger.warn( "JDBC::getItemType: No sqlType found for ItemType {}, use ItemType for STRINGITEM as Fallback for {}", diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/db/JdbcDerbyDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/db/JdbcDerbyDAO.java index 818fca918a6..a0c40efe0c0 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/db/JdbcDerbyDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/db/JdbcDerbyDAO.java @@ -72,6 +72,7 @@ public class JdbcDerbyDAO extends JdbcBaseDAO { // Prevent error against duplicate time value (seldom): No powerful Merge found: // http://www.codeproject.com/Questions/162627/how-to-insert-new-record-in-my-table-if-not-exists sqlInsertItemValue = "INSERT INTO #tableName# (TIME, VALUE) VALUES( #tablePrimaryValue#, CAST( ? as #dbType#) )"; + sqlAlterTableColumn = "ALTER TABLE #tableName# ALTER COLUMN #columnName# SET DATA TYPE #columnType#"; } private void initSqlTypes() { diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/db/JdbcPostgresqlDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/db/JdbcPostgresqlDAO.java index e12f0b91ae8..e26a069a2d5 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/db/JdbcPostgresqlDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/db/JdbcPostgresqlDAO.java @@ -68,6 +68,7 @@ public class JdbcPostgresqlDAO extends JdbcBaseDAO { // SQL_INSERT_ITEM_VALUE = "INSERT INTO #tableName# (TIME, VALUE) VALUES( NOW(), CAST( ? as #dbType#) ) ON // CONFLICT DO NOTHING"; sqlInsertItemValue = "INSERT INTO #tableName# (TIME, VALUE) VALUES( #tablePrimaryValue#, CAST( ? as #dbType#) )"; + sqlAlterTableColumn = "ALTER TABLE #tableName# ALTER COLUMN #columnName# TYPE #columnType#"; } /** diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/dto/Column.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/dto/Column.java new file mode 100644 index 00000000000..5f40cdab085 --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/dto/Column.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.persistence.jdbc.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Represents an INFORMATON_SCHEMA.COLUMNS table row. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class Column { + + private @Nullable String columnName; + private boolean isNullable; + private @Nullable String columnType; + + public String getColumnName() { + String columnName = this.columnName; + return columnName != null ? columnName : ""; + } + + public String getColumnType() { + String columnType = this.columnType; + return columnType != null ? columnType : ""; + } + + public boolean getIsNullable() { + return isNullable; + } + + public void setColumnName(String columnName) { + this.columnName = columnName; + } + + public void setColumnType(String columnType) { + this.columnType = columnType; + } + + public void setIsNullable(boolean isNullable) { + this.isNullable = isNullable; + } +}