From 70abb5d1f64efa17958ae32b9baab741efac53d1 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sat, 5 Nov 2022 10:41:31 +0100 Subject: [PATCH] [jdbc] Add support for case sensitive table names reflecting item names 1:1 (#13544) * Do not append number when using real item names * Extract getTableName to separate class * Add initial test coverage * Extract migration logic to separate class * Support migration from real names back to numbered * Simplify zero-padding * Fix NullPointerException * Fix MySQL compatibility when CLIENT_MULTI_STATEMENTS option is not set * Add option for case sensitive table names * Add real name with suffix mode for backwards compatibility * Remove real name in lower case without suffix mode * Map directly from item name to table name * Fix ambiguous table name scenario * Add additional testcase * Add migration path for changed table prefix * Drop items table when using direct mapping * Add configuration note * Fix table alignment * Extend description as more migration paths are now supported * Do not stop halfway through a migration * For clarity, do not use abbreviation for operating system Signed-off-by: Jacob Laursen --- .../org.openhab.persistence.jdbc/README.md | 66 +-- .../persistence/jdbc/db/JdbcBaseDAO.java | 29 +- .../persistence/jdbc/db/JdbcDerbyDAO.java | 2 +- .../persistence/jdbc/db/JdbcHsqldbDAO.java | 2 +- .../jdbc/db/JdbcPostgresqlDAO.java | 2 +- .../openhab/persistence/jdbc/dto/ItemsVO.java | 48 +- .../jdbc/internal/JdbcConfiguration.java | 20 + .../persistence/jdbc/internal/JdbcMapper.java | 190 ++++---- .../jdbc/internal/JdbcPersistenceService.java | 5 +- .../jdbc/internal/NamingStrategy.java | 120 +++++ .../main/resources/OH-INF/config/config.xml | 17 +- .../resources/OH-INF/i18n/jdbc.properties | 6 +- .../jdbc/internal/NamingStrategyTest.java | 444 ++++++++++++++++++ 13 files changed, 779 insertions(+), 172 deletions(-) create mode 100644 bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/NamingStrategy.java create mode 100644 bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/NamingStrategyTest.java diff --git a/bundles/org.openhab.persistence.jdbc/README.md b/bundles/org.openhab.persistence.jdbc/README.md index 0a828875f..1cf0ef8f6 100644 --- a/bundles/org.openhab.persistence.jdbc/README.md +++ b/bundles/org.openhab.persistence.jdbc/README.md @@ -38,38 +38,39 @@ The following databases are currently supported and tested: This service can be configured in the file `services/jdbc.cfg`. -| Property | Default | Required | Description | -| ------------------------- | ------------------------------------------------------------ | :-------: | ------------------------------------------------------------ | -| url | | Yes | JDBC URL to establish a connection to your database. Examples:

`jdbc:derby:./testDerby;create=true`
`jdbc:h2:./testH2`
`jdbc:hsqldb:./testHsqlDb`
`jdbc:mariadb://192.168.0.1:3306/testMariadb`
`jdbc:mysql://192.168.0.1:3306/testMysql?serverTimezone=UTC`
`jdbc:postgresql://192.168.0.1:5432/testPostgresql`
`jdbc:timescaledb://192.168.0.1:5432/testPostgresql`
`jdbc:sqlite:./testSqlite.db`.

If no database is available it will be created; for example the url `jdbc:h2:./testH2` creates a new H2 database in openHAB folder. Example to create your own MySQL database directly:

`CREATE DATABASE 'yourDB' CHARACTER SET utf8 COLLATE utf8_general_ci;` | -| user | | if needed | database user name | -| password | | if needed | database user password | -| errReconnectThreshold | 0 | No | when the service is deactivated (0 means ignore) | -| sqltype.CALL | `VARCHAR(200)` | No | All `sqlType` options allow you to change the SQL data type used to store values for different openHAB item states. See the following links for further information: [mybatis](https://mybatis.github.io/mybatis-3/apidocs/reference/org/apache/ibatis/type/JdbcType.html) [H2](https://www.h2database.com/html/datatypes.html) [PostgresSQL](https://www.postgresql.org/docs/9.3/static/datatype.html) | -| sqltype.COLOR | `VARCHAR(70)` | No | see above | -| sqltype.CONTACT | `VARCHAR(6)` | No | see above | -| sqltype.DATETIME | `DATETIME` | No | see above | -| sqltype.DIMMER | `TINYINT` | No | see above | -| sqltype.IMAGE | `VARCHAR(65500)` | No | see above | -| sqltype.LOCATION | `VARCHAR(50)` | No | see above | -| sqltype.NUMBER | `DOUBLE` | No | see above | -| sqltype.PLAYER | `VARCHAR(20)` | No | see above | -| sqltype.ROLLERSHUTTER | `TINYINT` | No | see above | -| sqltype.STRING | `VARCHAR(65500)` | No | see above | -| sqltype.SWITCH | `VARCHAR(6)` | No | see above | -| sqltype.tablePrimaryKey | `TIMESTAMP` | No | type of `time` column for newly created item tables | -| sqltype.tablePrimaryValue | `NOW()` | No | value of `time` column for newly inserted rows | -| numberDecimalcount | 3 | No | for Itemtype "Number" default decimal digit count | -| tableNamePrefix | `item` | No | table name prefix. For Migration from MySQL Persistence, set to `Item`. | -| tableUseRealItemNames | `false` | No | table name prefix generation. When set to `true`, real item names are used for table names and `tableNamePrefix` is ignored. When set to `false`, the `tableNamePrefix` is used to generate table names with sequential numbers. | -| tableIdDigitCount | 4 | No | when `tableUseRealItemNames` is `false` and thus table names are generated sequentially, this controls how many zero-padded digits are used in the table name. With the default of 4, the first table name will end with `0001`. For migration from the MySQL persistence service, set this to 0. | -| rebuildTableNames | false | No | rename existing tables using `tableUseRealItemNames` and `tableIdDigitCount`. USE WITH CARE! Deactivate after Renaming is done! | -| jdbc.maximumPoolSize | configured per database in package `org.openhab.persistence.jdbc.db.*` | No | Some embedded databases can handle only one connection. See [this link](https://github.com/brettwooldridge/HikariCP/issues/256) for more information | -| jdbc.minimumIdle | see above | No | see above | -| enableLogTime | `false` | No | timekeeping | +| Property | Default | Required | Description | +| --------------------------- | ------------------------------------------------------------ | :-------: | ------------------------------------------------------------ | +| url | | Yes | JDBC URL to establish a connection to your database. Examples:

`jdbc:derby:./testDerby;create=true`
`jdbc:h2:./testH2`
`jdbc:hsqldb:./testHsqlDb`
`jdbc:mariadb://192.168.0.1:3306/testMariadb`
`jdbc:mysql://192.168.0.1:3306/testMysql?serverTimezone=UTC`
`jdbc:postgresql://192.168.0.1:5432/testPostgresql`
`jdbc:timescaledb://192.168.0.1:5432/testPostgresql`
`jdbc:sqlite:./testSqlite.db`.

If no database is available it will be created; for example the url `jdbc:h2:./testH2` creates a new H2 database in openHAB folder. Example to create your own MySQL database directly:

`CREATE DATABASE 'yourDB' CHARACTER SET utf8 COLLATE utf8_general_ci;` | +| user | | if needed | database user name | +| password | | if needed | database user password | +| errReconnectThreshold | 0 | No | when the service is deactivated (0 means ignore) | +| sqltype.CALL | `VARCHAR(200)` | No | All `sqlType` options allow you to change the SQL data type used to store values for different openHAB item states. See the following links for further information: [mybatis](https://mybatis.github.io/mybatis-3/apidocs/reference/org/apache/ibatis/type/JdbcType.html) [H2](https://www.h2database.com/html/datatypes.html) [PostgresSQL](https://www.postgresql.org/docs/9.3/static/datatype.html) | +| sqltype.COLOR | `VARCHAR(70)` | No | see above | +| sqltype.CONTACT | `VARCHAR(6)` | No | see above | +| sqltype.DATETIME | `DATETIME` | No | see above | +| sqltype.DIMMER | `TINYINT` | No | see above | +| sqltype.IMAGE | `VARCHAR(65500)` | No | see above | +| sqltype.LOCATION | `VARCHAR(50)` | No | see above | +| sqltype.NUMBER | `DOUBLE` | No | see above | +| sqltype.PLAYER | `VARCHAR(20)` | No | see above | +| sqltype.ROLLERSHUTTER | `TINYINT` | No | see above | +| sqltype.STRING | `VARCHAR(65500)` | No | see above | +| sqltype.SWITCH | `VARCHAR(6)` | No | see above | +| sqltype.tablePrimaryKey | `TIMESTAMP` | No | type of `time` column for newly created item tables | +| sqltype.tablePrimaryValue | `NOW()` | No | value of `time` column for newly inserted rows | +| numberDecimalcount | 3 | No | for Itemtype "Number" default decimal digit count | +| tableNamePrefix | `item` | No | table name prefix. For Migration from MySQL Persistence, set to `Item`. | +| tableUseRealItemNames | `false` | No | table name prefix generation. When set to `true`, real item names are used for table names and `tableNamePrefix` is ignored. When set to `false`, the `tableNamePrefix` is used to generate table names with sequential numbers. | +| tableCaseSensitiveItemNames | `false` | No | table name case when `tableUseRealItemNames` is `true`. When set to `true`, item name case is preserved in table names and no suffix is used. When set to `false`, table names are lower cased and a numeric suffix is added. Please read [this](#case-sensitive-item-names) before enabling. | +| tableIdDigitCount | 4 | No | when `tableUseRealItemNames` is `false` and thus table names are generated sequentially, this controls how many zero-padded digits are used in the table name. With the default of 4, the first table name will end with `0001`. For migration from the MySQL persistence service, set this to 0. | +| rebuildTableNames | false | No | rename existing tables using `tableUseRealItemNames` and `tableIdDigitCount`. USE WITH CARE! Deactivate after Renaming is done! | +| jdbc.maximumPoolSize | configured per database in package `org.openhab.persistence.jdbc.db.*` | No | Some embedded databases can handle only one connection. See [this link](https://github.com/brettwooldridge/HikariCP/issues/256) for more information | +| jdbc.minimumIdle | see above | No | see above | +| enableLogTime | `false` | No | timekeeping | All item- and event-related configuration is done in the file `persistence/jdbc.persist`. -To configure this service as the default persistence service for openHAB 2, add or change the line +To configure this service as the default persistence service for openHAB, add or change the line ``` org.openhab.core.persistence:default=jdbc @@ -85,6 +86,13 @@ services/jdbc.cfg url=jdbc:postgresql://192.168.0.1:5432/testPostgresql ``` +### Case Sensitive Item Names + +To avoid numbered suffixes entirely, `tableUseRealItemNames` and `tableCaseSensitiveItemNames` must both be enabled. +With this configuration, tables are named exactly like their corresponding items. +In order for this to work correctly, the underlying operating system, database server and configuration must support case sensitive table names. +For MySQL, see [MySQL: Identifier Case Sensitivity](https://dev.mysql.com/doc/refman/8.0/en/identifier-case-sensitivity.html) for more information. + ### Migration from MySQL to JDBC Persistence Services The JDBC Persistence service can act as a replacement for the MySQL Persistence service. diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java index 63ed87172..074530b95 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java @@ -84,8 +84,9 @@ public class JdbcBaseDAO { protected String sqlIfTableExists = "SHOW TABLES LIKE '#searchTable#'"; protected String sqlCreateNewEntryInItemsTable = "INSERT INTO #itemsManageTable# (ItemName) VALUES ('#itemname#')"; protected String sqlCreateItemsTableIfNot = "CREATE TABLE IF NOT EXISTS #itemsManageTable# (ItemId INT NOT NULL AUTO_INCREMENT,#colname# #coltype# NOT NULL,PRIMARY KEY (ItemId))"; + protected String sqlDropItemsTableIfExists = "DROP TABLE IF EXISTS #itemsManageTable#"; protected String sqlDeleteItemsEntry = "DELETE FROM items WHERE ItemName=#itemname#"; - protected String sqlGetItemIDTableNames = "SELECT itemid, itemname FROM #itemsManageTable#"; + 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 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= ?"; @@ -266,7 +267,7 @@ public class JdbcBaseDAO { public Long doCreateNewEntryInItemsTable(ItemsVO vo) { String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable, new String[] { "#itemsManageTable#", "#itemname#" }, - new String[] { vo.getItemsManageTable(), vo.getItemname() }); + new String[] { vo.getItemsManageTable(), vo.getItemName() }); logger.debug("JDBC::doCreateNewEntryInItemsTable sql={}", sql); return Yank.insert(sql, null); } @@ -280,9 +281,17 @@ public class JdbcBaseDAO { return vo; } + public ItemsVO doDropItemsTableIfExists(ItemsVO vo) { + String sql = StringUtilsExt.replaceArrayMerge(sqlDropItemsTableIfExists, new String[] { "#itemsManageTable#" }, + new String[] { vo.getItemsManageTable() }); + logger.debug("JDBC::doDropItemsTableIfExists sql={}", sql); + Yank.execute(sql, null); + return vo; + } + public void doDeleteItemsEntry(ItemsVO vo) { String sql = StringUtilsExt.replaceArrayMerge(sqlDeleteItemsEntry, new String[] { "#itemname#" }, - new String[] { vo.getItemname() }); + new String[] { vo.getItemName() }); logger.debug("JDBC::doDeleteItemsEntry sql={}", sql); Yank.execute(sql, null); } @@ -306,8 +315,9 @@ public class JdbcBaseDAO { * ITEM DAOs * *************/ public void doUpdateItemTableNames(List vol) { - if (!vol.isEmpty()) { - String sql = updateItemTableNamesProvider(vol); + logger.debug("JDBC::doUpdateItemTableNames vol.size = {}", vol.size()); + for (ItemVO itemTable : vol) { + String sql = updateItemTableNamesProvider(itemTable); Yank.execute(sql, null); } } @@ -416,13 +426,8 @@ public class JdbcBaseDAO { return filterString; } - private String updateItemTableNamesProvider(List namesList) { - logger.debug("JDBC::updateItemTableNamesProvider namesList.size = {}", namesList.size()); - String queryString = ""; - for (int i = 0; i < namesList.size(); i++) { - ItemVO it = namesList.get(i); - queryString += "ALTER TABLE " + it.getTableName() + " RENAME TO " + it.getNewTableName() + ";"; - } + private String updateItemTableNamesProvider(ItemVO itemTable) { + String queryString = "ALTER TABLE " + itemTable.getTableName() + " RENAME TO " + itemTable.getNewTableName(); logger.debug("JDBC::query queryString = {}", queryString); return queryString; } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java index 4ee6c2d4c..f0d12bff0 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java @@ -123,7 +123,7 @@ public class JdbcDerbyDAO extends JdbcBaseDAO { public Long doCreateNewEntryInItemsTable(ItemsVO vo) { String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable, new String[] { "#itemsManageTable#", "#itemname#" }, - new String[] { vo.getItemsManageTable().toUpperCase(), vo.getItemname() }); + new String[] { vo.getItemsManageTable().toUpperCase(), vo.getItemName() }); logger.debug("JDBC::doCreateNewEntryInItemsTable sql={}", sql); return Yank.insert(sql, null); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java index 2fd77b4c7..fe9eb8f78 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java @@ -101,7 +101,7 @@ public class JdbcHsqldbDAO extends JdbcBaseDAO { public Long doCreateNewEntryInItemsTable(ItemsVO vo) { String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable, new String[] { "#itemsManageTable#", "#itemname#" }, - new String[] { vo.getItemsManageTable(), vo.getItemname() }); + new String[] { vo.getItemsManageTable(), vo.getItemName() }); logger.debug("JDBC::doCreateNewEntryInItemsTable sql={}", sql); return Yank.insert(sql, null); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java index 9c419af8c..974fd8fd0 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java @@ -121,7 +121,7 @@ public class JdbcPostgresqlDAO extends JdbcBaseDAO { public Long doCreateNewEntryInItemsTable(ItemsVO vo) { String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable, new String[] { "#itemsManageTable#", "#itemname#" }, - new String[] { vo.getItemsManageTable(), vo.getItemname() }); + new String[] { vo.getItemsManageTable(), vo.getItemName() }); logger.debug("JDBC::doCreateNewEntryInItemsTable sql={}", sql); return Yank.insert(sql, null); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemsVO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemsVO.java index 37ecc22b3..0e203f598 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemsVO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemsVO.java @@ -28,9 +28,9 @@ public class ItemsVO implements Serializable { private String coltype = "VARCHAR(500)"; private String colname = "itemname"; private String itemsManageTable = "items"; - private int itemid; - private String itemname; - private String table_name; + private int itemId; + private String itemName; + private String tableName; private String jdbcUriDatabaseName; public String getColtype() { @@ -57,28 +57,28 @@ public class ItemsVO implements Serializable { this.itemsManageTable = itemsManageTable.replaceAll(STR_FILTER, ""); } - public int getItemid() { - return itemid; + public int getItemId() { + return itemId; } - public void setItemid(int itemid) { - this.itemid = itemid; + public void setItemId(int itemId) { + this.itemId = itemId; } - public String getItemname() { - return itemname; + public String getItemName() { + return itemName; } - public void setItemname(String itemname) { - this.itemname = itemname; + public void setItemName(String itemName) { + this.itemName = itemName; } - public String getTable_name() { - return table_name; + public String getTableName() { + return tableName; } - public void setTable_name(String table_name) { - this.table_name = table_name; + public void setTableName(String tableName) { + this.tableName = tableName; } public String getJdbcUriDatabaseName() { @@ -98,8 +98,8 @@ public class ItemsVO implements Serializable { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((itemname == null) ? 0 : itemname.hashCode()); - result = prime * result + (itemid ^ (itemid >>> 32)); + result = prime * result + ((itemName == null) ? 0 : itemName.hashCode()); + result = prime * result + (itemId ^ (itemId >>> 32)); return result; } @@ -120,14 +120,14 @@ public class ItemsVO implements Serializable { return false; } ItemsVO other = (ItemsVO) obj; - if (itemname == null) { - if (other.itemname != null) { + if (itemName == null) { + if (other.itemName != null) { return false; } - } else if (!itemname.equals(other.itemname)) { + } else if (!itemName.equals(other.itemName)) { return false; } - return itemid == other.itemid; + return itemId == other.itemId; } @Override @@ -140,11 +140,11 @@ public class ItemsVO implements Serializable { builder.append(", itemsManageTable="); builder.append(itemsManageTable); builder.append(", itemid="); - builder.append(itemid); + builder.append(itemId); builder.append(", itemname="); - builder.append(itemname); + builder.append(itemName); builder.append(", table_name="); - builder.append(table_name); + builder.append(tableName); builder.append("]"); return builder.toString(); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java index a2342050c..56f76d745 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java @@ -57,6 +57,7 @@ public class JdbcConfiguration { // private String password; private int numberDecimalcount = 3; private boolean tableUseRealItemNames = false; + private boolean tableCaseSensitiveItemNames = false; private String tableNamePrefix = "item"; private int tableIdDigitCount = 4; private boolean rebuildTableNames = false; @@ -163,6 +164,12 @@ public class JdbcConfiguration { logger.debug("JDBC::updateConfig: tableUseRealItemNames={}", tableUseRealItemNames); } + String lc = (String) configuration.get("tableCaseSensitiveItemNames"); + if (lc != null && !lc.isBlank()) { + tableCaseSensitiveItemNames = Boolean.parseBoolean(lc); + logger.debug("JDBC::updateConfig: tableCaseSensitiveItemNames={}", tableCaseSensitiveItemNames); + } + String td = (String) configuration.get("tableIdDigitCount"); if (td != null && !td.isBlank() && isNumericPattern.matcher(td).matches()) { tableIdDigitCount = Integer.parseInt(td); @@ -363,6 +370,19 @@ public class JdbcConfiguration { return tableUseRealItemNames; } + public boolean getTableCaseSensitiveItemNames() { + return tableCaseSensitiveItemNames; + } + + /** + * Checks if real item names (without number suffix) is enabled. + * + * @return true if both tableUseRealItemNames and tableCaseSensitiveItemNames are enabled. + */ + public boolean getTableUseRealCaseSensitiveItemNames() { + return tableUseRealItemNames && tableCaseSensitiveItemNames; + } + public int getTableIdDigitCount() { return tableIdDigitCount; } 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 f3aa81a4d..f59e7c8b6 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 @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -53,10 +54,10 @@ public class JdbcMapper { protected int errCnt; protected boolean initialized = false; protected @NonNullByDefault({}) JdbcConfiguration conf; - protected final Map sqlTables = new HashMap<>(); + protected final Map itemNameToTableNameMap = new HashMap<>(); + protected @NonNullByDefault({}) NamingStrategy namingStrategy; private long afterAccessMin = 10000; private long afterAccessMax = 0; - private static final String ITEM_NAME_PATTERN = "[^a-zA-Z_0-9\\-]"; public JdbcMapper(TimeZoneProvider timeZoneProvider) { this.timeZoneProvider = timeZoneProvider; @@ -97,11 +98,19 @@ public class JdbcMapper { return res != null ? res : ""; } + public boolean ifItemsTableExists() { + logger.debug("JDBC::ifItemsTableExists"); + long timerStart = System.currentTimeMillis(); + boolean res = conf.getDBDAO().doIfTableExists(new ItemsVO()); + logTime("doIfTableExists", timerStart, System.currentTimeMillis()); + return res; + } + public ItemsVO createNewEntryInItemsTable(ItemsVO vo) { logger.debug("JDBC::createNewEntryInItemsTable"); long timerStart = System.currentTimeMillis(); Long i = conf.getDBDAO().doCreateNewEntryInItemsTable(vo); - vo.setItemid(i.intValue()); + vo.setItemId(i.intValue()); logTime("doCreateNewEntryInItemsTable", timerStart, System.currentTimeMillis()); return vo; } @@ -114,6 +123,14 @@ public class JdbcMapper { return true; } + public boolean dropItemsTableIfExists(ItemsVO vo) { + logger.debug("JDBC::dropItemsTableIfExists"); + long timerStart = System.currentTimeMillis(); + conf.getDBDAO().doDropItemsTableIfExists(vo); + logTime("doDropItemsTableIfExists", timerStart, System.currentTimeMillis()); + return true; + } + public ItemsVO deleteItemsEntry(ItemsVO vo) { logger.debug("JDBC::deleteItemsEntry"); long timerStart = System.currentTimeMillis(); @@ -252,47 +269,66 @@ public class JdbcMapper { * DATABASE TABLEHANDLING * **************************/ protected void checkDBSchema() { - // Create Items Table if does not exist - createItemsTableIfNot(new ItemsVO()); + if (!conf.getTableUseRealCaseSensitiveItemNames()) { + createItemsTableIfNot(new ItemsVO()); + } if (conf.getRebuildTableNames()) { formatTableNames(); + + if (conf.getTableUseRealCaseSensitiveItemNames()) { + dropItemsTableIfExists(new ItemsVO()); + } logger.info( "JDBC::checkDBSchema: Rebuild complete, configure the 'rebuildTableNames' setting to 'false' to stop rebuilds on startup"); - } else { // Reset the error counter errCnt = 0; + } + populateItemNameToTableNameMap(); + } + + private void populateItemNameToTableNameMap() { + itemNameToTableNameMap.clear(); + if (conf.getTableUseRealCaseSensitiveItemNames()) { + for (String itemName : getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList())) { + itemNameToTableNameMap.put(itemName, itemName); + } + } else { for (ItemsVO vo : getItemIDTableNames()) { - sqlTables.put(vo.getItemname(), getTableName(vo.getItemid(), vo.getItemname())); + itemNameToTableNameMap.put(vo.getItemName(), + namingStrategy.getTableName(vo.getItemId(), vo.getItemName())); } } } protected String getTable(Item item) { - int rowId = 0; + int itemId = 0; ItemsVO isvo; ItemVO ivo; String itemName = item.getName(); - String tableName = sqlTables.get(itemName); + String tableName = itemNameToTableNameMap.get(itemName); // Table already exists - return the name - if (tableName != null) { + if (!Objects.isNull(tableName)) { return tableName; } logger.debug("JDBC::getTable: no table found for item '{}' in sqlTables", itemName); - // Create a new entry in items table - isvo = new ItemsVO(); - isvo.setItemname(itemName); - isvo = createNewEntryInItemsTable(isvo); - rowId = isvo.getItemid(); - if (rowId == 0) { - logger.error("JDBC::getTable: Creating table for item '{}' failed.", itemName); + if (!conf.getTableUseRealCaseSensitiveItemNames()) { + // Create a new entry in items table + isvo = new ItemsVO(); + isvo.setItemName(itemName); + isvo = createNewEntryInItemsTable(isvo); + itemId = isvo.getItemId(); + if (itemId == 0) { + logger.error("JDBC::getTable: Creating items entry for item '{}' failed.", itemName); + } } + // Create the table name - logger.debug("JDBC::getTable: getTableName with rowId={} itemName={}", rowId, itemName); - tableName = getTableName(rowId, itemName); + logger.debug("JDBC::getTable: getTableName with rowId={} itemName={}", itemId, itemName); + tableName = namingStrategy.getTableName(itemId, itemName); // Create table for item String dataType = conf.getDBDAO().getDataType(item); @@ -301,18 +337,8 @@ public class JdbcMapper { ivo = createItemTable(ivo); logger.debug("JDBC::getTable: Table created for item '{}' with dataType {} in SQL database.", itemName, dataType); - sqlTables.put(itemName, tableName); - // Check if the new entry is in the table list - // If it's not in the list, then there was an error and we need to do - // some tidying up - // The item needs to be removed from the index table to avoid duplicates - if (sqlTables.get(itemName) == null) { - logger.error("JDBC::getTable: Item '{}' was not added to the table - removing index", itemName); - isvo = new ItemsVO(); - isvo.setItemname(itemName); - deleteItemsEntry(isvo); - } + itemNameToTableNameMap.put(itemName, tableName); return tableName; } @@ -323,93 +349,57 @@ public class JdbcMapper { initialized = false; } - Map tableIds = new HashMap<>(); + List itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList(); + List itemTables = getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList()); + List oldNewTableNames; - // - for (ItemsVO vo : getItemIDTableNames()) { - String t = getTableName(vo.getItemid(), vo.getItemname()); - sqlTables.put(vo.getItemname(), t); - tableIds.put(vo.getItemid(), t); - } - - // - List al = getItemTables(); - - String oldName = ""; - String newName = ""; - List oldNewTablenames = new ArrayList<>(); - for (int i = 0; i < al.size(); i++) { - int id = -1; - oldName = al.get(i).getTable_name(); - logger.info("JDBC::formatTableNames: found Table Name= {}", oldName); - - if (oldName.startsWith(conf.getTableNamePrefix()) && !oldName.contains("_")) { - id = Integer.parseInt(oldName.substring(conf.getTableNamePrefix().length())); - logger.info("JDBC::formatTableNames: found Table with Prefix '{}' Name= {} id= {}", - conf.getTableNamePrefix(), oldName, (id)); - } else if (oldName.contains("_")) { - id = Integer.parseInt(oldName.substring(oldName.lastIndexOf("_") + 1)); - logger.info("JDBC::formatTableNames: found Table Name= {} id= {}", oldName, (id)); + if (itemIdTableNames.isEmpty()) { + // Without mappings we can only migrate from direct item name to numeric mapping. + if (conf.getTableUseRealCaseSensitiveItemNames()) { + logger.info("JDBC::formatTableNames: Nothing to migrate."); + initialized = tmpinit; + return; } - logger.info("JDBC::formatTableNames: found Table id= {}", id); - - newName = tableIds.get(id); - logger.info("JDBC::formatTableNames: found Table newName= {}", newName); - - if (newName != null) { - if (!oldName.equalsIgnoreCase(newName)) { - oldNewTablenames.add(new ItemVO(oldName, newName)); - logger.info("JDBC::formatTableNames: Table '{}' will be renamed to '{}'", oldName, newName); + oldNewTableNames = new ArrayList<>(); + for (String itemName : itemTables) { + ItemsVO isvo = new ItemsVO(); + isvo.setItemName(itemName); + isvo = createNewEntryInItemsTable(isvo); + int itemId = isvo.getItemId(); + if (itemId == 0) { + logger.error("JDBC::formatTableNames: Creating items entry for item '{}' failed.", itemName); } else { - logger.info("JDBC::formatTableNames: Table oldName='{}' newName='{}' nothing to rename", oldName, - newName); + String newTableName = namingStrategy.getTableName(itemId, itemName); + oldNewTableNames.add(new ItemVO(itemName, newTableName)); + logger.info("JDBC::formatTableNames: Table '{}' will be renamed to '{}'", itemName, newTableName); } - } else { - logger.error("JDBC::formatTableNames: Table '{}' could NOT be renamed to '{}'", oldName, newName); - break; } + } else { + String itemsManageTable = new ItemsVO().getItemsManageTable(); + Map itemIdToItemNameMap = new HashMap<>(); + + for (ItemsVO vo : itemIdTableNames) { + int itemId = vo.getItemId(); + String itemName = vo.getItemName(); + itemIdToItemNameMap.put(itemId, itemName); + } + + oldNewTableNames = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, itemsManageTable); } - updateItemTableNames(oldNewTablenames); - logger.info("JDBC::formatTableNames: Finished updating {} item table names", oldNewTablenames.size()); + updateItemTableNames(oldNewTableNames); + logger.info("JDBC::formatTableNames: Finished updating {} item table names", oldNewTableNames.size()); initialized = tmpinit; } - private String getTableName(int rowId, String itemName) { - return getTableNamePrefix(itemName) + formatRight(rowId, conf.getTableIdDigitCount()); - } - - private String getTableNamePrefix(String itemName) { - String name = conf.getTableNamePrefix(); - if (conf.getTableUseRealItemNames()) { - // Create the table name with real Item Names - name = (itemName.replaceAll(ITEM_NAME_PATTERN, "") + "_").toLowerCase(); - } - return name; - } - public Set getItems() { // TODO: in general it would be possible to query the count, earliest and latest values for each item too but it // would be a very costly operation - return sqlTables.keySet().stream().map(itemName -> new JdbcPersistenceItemInfo(itemName)) + return itemNameToTableNameMap.keySet().stream().map(itemName -> new JdbcPersistenceItemInfo(itemName)) .collect(Collectors. toSet()); } - private static String formatRight(final Object value, final int len) { - final String valueAsString = String.valueOf(value); - if (valueAsString.length() < len) { - final StringBuffer result = new StringBuffer(len); - for (int i = len - valueAsString.length(); i > 0; i--) { - result.append('0'); - } - result.append(valueAsString); - return result.toString(); - } else { - return valueAsString; - } - } - /***************** * H E L P E R S * *****************/ 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 20a0fff89..bac25c3e5 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 @@ -206,7 +206,7 @@ public class JdbcPersistenceService extends JdbcMapper implements ModifiablePers } } - String table = sqlTables.get(itemName); + String table = itemNameToTableNameMap.get(itemName); if (table == null) { logger.debug("JDBC::query: unable to find table for item with name: '{}', no data in database.", itemName); return List.of(); @@ -229,6 +229,7 @@ public class JdbcPersistenceService extends JdbcMapper implements ModifiablePers conf = new JdbcConfiguration(configuration); if (conf.valid && checkDBAccessability()) { + namingStrategy = new NamingStrategy(conf); checkDBSchema(); // connection has been established ... initialization completed! initialized = true; @@ -259,7 +260,7 @@ public class JdbcPersistenceService extends JdbcMapper implements ModifiablePers throw new IllegalArgumentException("Item name must not be null"); } - String table = sqlTables.get(itemName); + String table = itemNameToTableNameMap.get(itemName); if (table == null) { logger.debug("JDBC::remove: unable to find table for item with name: '{}', no data in database.", itemName); return false; diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/NamingStrategy.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/NamingStrategy.java new file mode 100644 index 000000000..3fcf9f065 --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/NamingStrategy.java @@ -0,0 +1,120 @@ +/** + * 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; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.items.ItemUtil; +import org.openhab.persistence.jdbc.dto.ItemVO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class manages strategy for table names. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class NamingStrategy { + + private final Logger logger = LoggerFactory.getLogger(NamingStrategy.class); + + private JdbcConfiguration configuration; + + public NamingStrategy(JdbcConfiguration configuration) { + this.configuration = configuration; + } + + public String getTableName(int itemId, String itemName) { + if (!ItemUtil.isValidItemName(itemName)) { + throw new IllegalArgumentException(itemName + " is not a valid item name"); + } + if (configuration.getTableUseRealItemNames()) { + return formatTableName(itemName, itemId); + } else { + return configuration.getTableNamePrefix() + getSuffix(itemId); + } + } + + private String formatTableName(String itemName, int itemId) { + if (configuration.getTableCaseSensitiveItemNames()) { + return itemName; + } else { + return itemName.toLowerCase() + "_" + getSuffix(itemId); + } + } + + private String getSuffix(int itemId) { + int digits = configuration.getTableIdDigitCount(); + if (digits > 0) { + return String.format("%0" + configuration.getTableIdDigitCount() + "d", itemId); + } else { + return String.valueOf(itemId); + } + } + + public List prepareMigration(List itemTables, Map itemIdToItemNameMap, + String itemsManageTable) { + List oldNewTableNames = new ArrayList<>(); + Map tableNameToItemIdMap = new HashMap<>(); + + for (Entry entry : itemIdToItemNameMap.entrySet()) { + String itemName = entry.getValue(); + tableNameToItemIdMap.put(itemName, entry.getKey()); + } + + for (String oldName : itemTables) { + Integer itemIdBoxed = tableNameToItemIdMap.get(oldName); + int itemId = -1; + + if (Objects.nonNull(itemIdBoxed)) { + itemId = itemIdBoxed; + logger.info("JDBC::formatTableNames: found by name; table name= {} id= {}", oldName, itemId); + } else { + try { + itemId = Integer.parseInt(oldName.replaceFirst("^.*\\D", "")); + logger.info("JDBC::formatTableNames: found by id; table name= {} id= {}", oldName, itemId); + } catch (NumberFormatException e) { + // Fall through. + } + } + + String itemName = itemIdToItemNameMap.get(itemId); + + if (!Objects.isNull(itemName)) { + String newName = getTableName(itemId, itemName); + if (newName.equalsIgnoreCase(itemsManageTable)) { + logger.error( + "JDBC::formatTableNames: Table '{}' could NOT be renamed to '{}' since it conflicts with manage table", + oldName, newName); + } else if (!oldName.equals(newName)) { + oldNewTableNames.add(new ItemVO(oldName, newName)); + logger.info("JDBC::formatTableNames: Table '{}' will be renamed to '{}'", oldName, newName); + } else { + logger.info("JDBC::formatTableNames: Table oldName='{}' newName='{}' nothing to rename", oldName, + newName); + } + } else { + logger.error("JDBC::formatTableNames: Table '{}' could NOT be renamed for id '{}'", oldName, itemId); + } + } + + return oldNewTableNames; + } +} diff --git a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/config/config.xml index 31d16a61b..cc8411376 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/config/config.xml @@ -143,6 +143,11 @@ #tableUseRealItemNames= tableUseRealItemNames=true + # Tablename Prefix generation, using case sensitive item names (optional, default: disabled -> table names are lower cased + # with numeric suffix appended). + # If true, no suffix is used. + #tableCaseSensitiveItemNames=true + # Tablename Suffix length (optional, default: 4 -> 0001-9999) # for Migration from MYSQL-Bundle set to 0. #tableIdDigitCount= @@ -165,6 +170,15 @@ + + + + If true, no suffix is used. (optional, default: disabled -> table names are lower cased with numeric suffix appended).]]> + + + + + (optional, default: 4 -> 0001-9999).
@@ -172,7 +186,8 @@
- + USE WITH CARE! Deactivate after renaming is done!]]> diff --git a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc.properties b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc.properties index 110e4b8d9..670c072cc 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc.properties +++ b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc.properties @@ -9,7 +9,7 @@ persistence.config.jdbc.minimumIdle.description = Overrides min idle database co persistence.config.jdbc.password.label = Database Password persistence.config.jdbc.password.description = Defines the database password. persistence.config.jdbc.rebuildTableNames.label = Tablename Rebuild -persistence.config.jdbc.rebuildTableNames.description = Rename existing tables using 'Tablename Realname Generation' and 'Tablename Suffix ID Count', (optional, default: disabled).
USE WITH CARE! Deactivate after renaming is done! +persistence.config.jdbc.rebuildTableNames.description = Rename existing tables using 'Tablename Prefix String', 'Tablename Realname Generation', 'Tablename Case Sensitive' and 'Tablename Suffix ID Count'. (optional, default: disabled).
USE WITH CARE! Deactivate after renaming is done! persistence.config.jdbc.rebuildTableNames.option.true = Enable persistence.config.jdbc.rebuildTableNames.option.false = Disable persistence.config.jdbc.sqltype.CALL.label = SqlType CALL @@ -36,6 +36,10 @@ persistence.config.jdbc.sqltype.STRING.label = SqlType STRING persistence.config.jdbc.sqltype.STRING.description = Overrides used JDBC/SQL datatype for STRING
(optional, default: "VARCHAR(65500)"). persistence.config.jdbc.sqltype.SWITCH.label = SqlType SWITCH persistence.config.jdbc.sqltype.SWITCH.description = Overrides used JDBC/SQL datatype for SWITCH
(optional, default: "VARCHAR(6)"). +persistence.config.jdbc.tableCaseSensitiveItemNames.label = Tablename Case Sensitive +persistence.config.jdbc.tableCaseSensitiveItemNames.description = Enables Tablename generation with case sensitive item names case when "Tablename Realname Generation" is enabled
If true, no suffix is used. (optional, default: disabled -> table names are lower cased with numeric suffix appended). +persistence.config.jdbc.tableCaseSensitiveItemNames.option.true = Enable +persistence.config.jdbc.tableCaseSensitiveItemNames.option.false = Disable persistence.config.jdbc.tableIdDigitCount.label = Tablename Suffix ID Count persistence.config.jdbc.tableIdDigitCount.description = Tablename Suffix ID Count
(optional, default: 4 -> 0001-9999).
For migration from MYSQL-Bundle set to 0. persistence.config.jdbc.tableNamePrefix.label = Tablename Prefix String diff --git a/bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/NamingStrategyTest.java b/bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/NamingStrategyTest.java new file mode 100644 index 000000000..257bf97ac --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/NamingStrategyTest.java @@ -0,0 +1,444 @@ +/** + * 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; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.persistence.jdbc.dto.ItemVO; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; + +/** + * Tests the {@link NamingStrategy} class. + * + * @author Jacob Laursen - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class NamingStrategyTest { + private static final String ITEMS_MANAGE_TABLE_NAME = "items"; + + private @Mock @NonNullByDefault({}) JdbcConfiguration configurationMock; + private NamingStrategy namingStrategy = new NamingStrategy(configurationMock); + + @BeforeEach + public void initialize() { + final Logger logger = (Logger) LoggerFactory.getLogger(NamingStrategy.class); + logger.setLevel(Level.OFF); + namingStrategy = new NamingStrategy(configurationMock); + } + + @Test + public void getTableNameWhenInvalidItemNameThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + namingStrategy.getTableName(1, "4Two"); + }); + } + + @Test + public void getTableNameWhenUseRealItemNamesNameIsLowerCaseAndNumbered() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(false).when(configurationMock).getTableCaseSensitiveItemNames(); + assertThat(namingStrategy.getTableName(1, "Test"), is("test_1")); + } + + @Test + public void getTableNameWhenUseRealCaseSensitiveItemNamesNameIsSameCase() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(true).when(configurationMock).getTableCaseSensitiveItemNames(); + assertThat(namingStrategy.getTableName(1, "Camel"), is("Camel")); + } + + @Test + public void getTableNameWhenUseRealCaseSensitiveItemNamesNameIsSameCaseLower() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(true).when(configurationMock).getTableCaseSensitiveItemNames(); + assertThat(namingStrategy.getTableName(1, "lower"), is("lower")); + } + + @Test + public void getTableNameWhenNotUseRealItemNamesAndCount4NameHasLeavingZeros() { + Mockito.doReturn(false).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(4).when(configurationMock).getTableIdDigitCount(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + assertThat(namingStrategy.getTableName(2, "Test"), is("Item0002")); + } + + @Test + public void getTableNameWhenNotUseRealItemNamesAndCount0() { + Mockito.doReturn(false).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(0).when(configurationMock).getTableIdDigitCount(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + assertThat(namingStrategy.getTableName(12345, "Test"), is("Item12345")); + } + + @Test + public void prepareMigrationFromNumberedToRealNames() { + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "Item1"; + + List actual = prepareMigrationRealItemNames(itemId, itemName, tableName); + + assertTableName(actual, "Test"); + } + + @Test + public void prepareMigrationWithChangedPrefix() { + Mockito.doReturn(0).when(configurationMock).getTableIdDigitCount(); + Mockito.doReturn(false).when(configurationMock).getTableUseRealItemNames(); + + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "Item1"; + + List actual = prepareMigration(itemId, itemName, tableName, "item"); + + assertTableName(actual, "item1"); + } + + @Test + public void prepareMigrationShouldNotStopWhenEncounteringUnknownItem() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(true).when(configurationMock).getTableCaseSensitiveItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + + Map itemIdToItemNameMap = new HashMap<>(2); + itemIdToItemNameMap.put(1, "First"); + itemIdToItemNameMap.put(3, "Third"); + + List itemTables = new ArrayList(3); + itemTables.add("Item1"); + itemTables.add("Item2"); + itemTables.add("Item3"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(2)); + assertThat(actual.get(0).getNewTableName(), is("First")); + assertThat(actual.get(1).getNewTableName(), is("Third")); + } + + @Test + public void prepareMigrationFromMixedNumberedToNumberedRealNames() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(false).when(configurationMock).getTableCaseSensitiveItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + + Map itemIdToItemNameMap = new HashMap<>(3); + itemIdToItemNameMap.put(1, "First"); + itemIdToItemNameMap.put(2, "Second"); + itemIdToItemNameMap.put(3, "Third"); + + List itemTables = new ArrayList(3); + itemTables.add("Item1"); + itemTables.add("Item002"); + itemTables.add("third_0003"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(3)); + assertThat(actual.get(0).getNewTableName(), is("first_1")); + assertThat(actual.get(1).getNewTableName(), is("second_2")); + assertThat(actual.get(2).getNewTableName(), is("third_3")); + } + + @Test + public void prepareMigrationFromMixedNumberedToCaseSensitiveRealNames() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(true).when(configurationMock).getTableCaseSensitiveItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + + Map itemIdToItemNameMap = new HashMap<>(3); + itemIdToItemNameMap.put(1, "First"); + itemIdToItemNameMap.put(2, "Second"); + itemIdToItemNameMap.put(3, "Third"); + + List itemTables = new ArrayList(3); + itemTables.add("Item1"); + itemTables.add("Item002"); + itemTables.add("third_0003"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(3)); + assertThat(actual.get(0).getNewTableName(), is("First")); + assertThat(actual.get(1).getNewTableName(), is("Second")); + assertThat(actual.get(2).getNewTableName(), is("Third")); + } + + @Test + public void prepareMigrationFromNumberedRealNamesToCaseSensitiveRealNames() { + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "test_0001"; + + List actual = prepareMigrationRealItemNames(itemId, itemName, tableName, true); + + assertTableName(actual, "Test"); + } + + @Test + public void prepareMigrationFromCaseSensitiveRealNamesToNumberedRealNames() { + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "Test"; + + List actual = prepareMigrationRealItemNames(itemId, itemName, tableName, false); + + assertTableName(actual, "test_0001"); + } + + @Test + public void prepareMigrationRealNamesWithTwoItemsWithDifferentCaseToNumbered() { + Mockito.doReturn(false).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + Mockito.doReturn(1).when(configurationMock).getTableIdDigitCount(); + + Map itemIdToItemNameMap = new HashMap<>(2); + itemIdToItemNameMap.put(1, "MyItem"); + itemIdToItemNameMap.put(2, "myItem"); + + List itemTables = new ArrayList(2); + itemTables.add("MyItem"); + itemTables.add("myItem"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(2)); + assertThat(actual.get(0).getNewTableName(), is("Item1")); + assertThat(actual.get(1).getNewTableName(), is("Item2")); + } + + @Test + public void prepareMigrationNumberedWithTwoItemsWithDifferentCaseToNumberedRealNames() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + Mockito.doReturn(false).when(configurationMock).getTableCaseSensitiveItemNames(); + + Map itemIdToItemNameMap = new HashMap<>(2); + itemIdToItemNameMap.put(1, "MyItem"); + itemIdToItemNameMap.put(2, "myItem"); + + List itemTables = new ArrayList(2); + itemTables.add("Item1"); + itemTables.add("Item2"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(2)); + assertThat(actual.get(0).getNewTableName(), is("myitem_1")); + assertThat(actual.get(1).getNewTableName(), is("myitem_2")); + } + + @Test + public void prepareMigrationNumberedWithTwoItemsWithDifferentCaseToCaseSensitiveRealNames() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + Mockito.doReturn(true).when(configurationMock).getTableCaseSensitiveItemNames(); + + Map itemIdToItemNameMap = new HashMap<>(2); + itemIdToItemNameMap.put(1, "MyItem"); + itemIdToItemNameMap.put(2, "myItem"); + + List itemTables = new ArrayList(2); + itemTables.add("Item1"); + itemTables.add("Item2"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(2)); + assertThat(actual.get(0).getNewTableName(), is("MyItem")); + assertThat(actual.get(1).getNewTableName(), is("myItem")); + } + + @Test + public void prepareMigrationFromNumberedRealNamesToCaseSensitiveRealNamesWhenUnknownItemIdThenSkip() { + final int itemId = 2; + final String itemName = "Test"; + final String tableName = "test_0001"; + + List actual = prepareMigrationRealItemNames(itemId, itemName, tableName); + + assertThat(actual.size(), is(0)); + } + + @Test + public void prepareMigrationFromNumberedRealNamesToNumbered() { + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "test_0001"; + + List actual = prepareMigrationNumbered(itemId, itemName, tableName); + + assertTableName(actual, "Item0001"); + } + + @Test + public void prepareMigrationFromNumberedToNumberedWithCorrectPadding() { + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "Item1"; + + List actual = prepareMigrationNumbered(itemId, itemName, tableName, 2); + + assertTableName(actual, "Item01"); + } + + @Test + public void prepareMigrationFromNumberedToNumberedExceedingPadding() { + final int itemId = 101; + final String itemName = "Test"; + final String tableName = "Item0101"; + + List actual = prepareMigrationNumbered(itemId, itemName, tableName, 2); + + assertTableName(actual, "Item101"); + } + + @Test + public void prepareMigrationFromCaseSensitiveRealNamesToNumbered() { + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "Test"; + + List actual = prepareMigrationNumbered(itemId, itemName, tableName); + + assertTableName(actual, "Item0001"); + } + + @Test + public void prepareMigrationFromCaseSensitiveRealNamesToNumberedHavingUnderscore() { + final int itemId = 1; + final String itemName = "My_Test"; + final String tableName = "My_Test"; + + List actual = prepareMigrationNumbered(itemId, itemName, tableName); + + assertTableName(actual, "Item0001"); + } + + @Test + public void prepareMigrationFromCaseSensitiveRealNamesHavingUnderscoreAndNumberToNumbered() { + final int itemId = 2; + final String itemName = "My_Test_1"; + final String tableName = "My_Test_1"; + + List actual = prepareMigrationNumbered(itemId, itemName, tableName); + + assertTableName(actual, "Item0002"); + } + + @Test + public void prepareMigrationFromCaseSensitiveRealNamesToNumberedShouldSwap() { + Mockito.doReturn(false).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + + Map itemIdToItemNameMap = new HashMap<>(2); + itemIdToItemNameMap.put(1, "Item2"); + itemIdToItemNameMap.put(2, "Item1"); + + List itemTables = new ArrayList(2); + itemTables.add("Item2"); + itemTables.add("Item1"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(2)); + assertThat(actual.get(0).getNewTableName(), is("Item1")); + assertThat(actual.get(1).getNewTableName(), is("Item2")); + } + + @Test + public void prepareMigrationWhenConflictWithItemsManageTableThenSkip() { + final int itemId = 1; + final String itemName = "items"; + final String tableName = "Item1"; + + List actual = prepareMigrationRealItemNames(itemId, itemName, tableName); + + assertThat(actual.size(), is(0)); + } + + private List prepareMigrationNumbered(int itemId, String itemName, String tableName) { + return prepareMigrationNumbered(itemId, itemName, tableName, 4); + } + + private List prepareMigrationNumbered(int itemId, String itemName, String tableName, + int tableIdDigitCount) { + Mockito.doReturn(tableIdDigitCount).when(configurationMock).getTableIdDigitCount(); + Mockito.doReturn(false).when(configurationMock).getTableUseRealItemNames(); + return prepareMigration(itemId, itemName, tableName); + } + + private List prepareMigrationRealItemNames(int itemId, String itemName, String tableName) { + return prepareMigrationRealItemNames(itemId, itemName, tableName, true); + } + + private List prepareMigrationRealItemNames(int itemId, String itemName, String tableName, + boolean caseSensitive) { + Mockito.doReturn(4).when(configurationMock).getTableIdDigitCount(); + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(caseSensitive).when(configurationMock).getTableCaseSensitiveItemNames(); + return prepareMigration(itemId, itemName, tableName); + } + + private List prepareMigration(int itemId, String itemName, String tableName) { + return prepareMigration(itemId, itemName, tableName, "Item"); + } + + private List prepareMigration(int itemId, String itemName, String tableName, String prefix) { + Mockito.doReturn(prefix).when(configurationMock).getTableNamePrefix(); + + Map itemIdToItemNameMap = getItemIdToItemNameMap(itemId, itemName); + List itemTables = getItemTables(tableName); + + return namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + } + + private Map getItemIdToItemNameMap(int itemId, String itemName) { + Map itemIdToItemNameMap = new HashMap<>(1); + itemIdToItemNameMap.put(itemId, itemName); + return itemIdToItemNameMap; + } + + private List getItemTables(String tableName) { + List itemTables = new ArrayList(1); + itemTables.add(tableName); + return itemTables; + } + + private void assertTableName(List actual, String expected) { + assertThat(actual.size(), is(1)); + assertThat(actual.get(0).getNewTableName(), is(expected)); + } +}