]> git.basschouten.com Git - openhab-addons.git/commitdiff
[jdbc] Add support for case sensitive table names reflecting item names 1:1 (#13544)
authorJacob Laursen <jacob-github@vindvejr.dk>
Sat, 5 Nov 2022 09:41:31 +0000 (10:41 +0100)
committerGitHub <noreply@github.com>
Sat, 5 Nov 2022 09:41:31 +0000 (10:41 +0100)
* 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 <jacob-github@vindvejr.dk>
13 files changed:
bundles/org.openhab.persistence.jdbc/README.md
bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java
bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java
bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java
bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java
bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemsVO.java
bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java
bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java
bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java
bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/NamingStrategy.java [new file with mode: 0644]
bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/config/config.xml
bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc.properties
bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/NamingStrategyTest.java [new file with mode: 0644]

index 0a828875f8741483f88dbdf6de2edb63e0d27a7c..1cf0ef8f6903ecf3eda0b010ccdc19adb371bd33 100644 (file)
@@ -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:<br/><br/>`jdbc:derby:./testDerby;create=true`<br/>`jdbc:h2:./testH2`<br/>`jdbc:hsqldb:./testHsqlDb`<br/>`jdbc:mariadb://192.168.0.1:3306/testMariadb`<br/>`jdbc:mysql://192.168.0.1:3306/testMysql?serverTimezone=UTC`<br/>`jdbc:postgresql://192.168.0.1:5432/testPostgresql`<br/>`jdbc:timescaledb://192.168.0.1:5432/testPostgresql`<br/>`jdbc:sqlite:./testSqlite.db`.<br/><br/>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:<br/><br/>`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:<br/><br/>`jdbc:derby:./testDerby;create=true`<br/>`jdbc:h2:./testH2`<br/>`jdbc:hsqldb:./testHsqlDb`<br/>`jdbc:mariadb://192.168.0.1:3306/testMariadb`<br/>`jdbc:mysql://192.168.0.1:3306/testMysql?serverTimezone=UTC`<br/>`jdbc:postgresql://192.168.0.1:5432/testPostgresql`<br/>`jdbc:timescaledb://192.168.0.1:5432/testPostgresql`<br/>`jdbc:sqlite:./testSqlite.db`.<br/><br/>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:<br/><br/>`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.
index 63ed8717273a3f864e9146793ca68897359f2beb..074530b953810697490d378c766303951aa7f1de 100644 (file)
@@ -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<ItemVO> 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<ItemVO> 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;
     }
index 4ee6c2d4cedb8c5ed0d0e47833c3f40507a3551f..f0d12bff03bddc48800cd24921432fc46be7104a 100644 (file)
@@ -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);
     }
index 2fd77b4c78f36c32c1d72451e64cf54fa8a439cc..fe9eb8f78e92efc0a07f597c3914463c358c0ceb 100644 (file)
@@ -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);
     }
index 9c419af8c2c587c4df815dad6d0dc46b76d5da41..974fd8fd0bf7b2ebfaacc60d9283e0eb9b4e6db3 100644 (file)
@@ -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);
     }
index 37ecc22b38d3bac400d5584cc98e1e79f817663b..0e203f59817701f3f09d4612915e7eb39bcdef3a 100644 (file)
@@ -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();
     }
index a2342050c77abd6b54d9ae4309c0eb5171e8b6f2..56f76d745f3efe611dbdd81a4a5c77b7672a5466 100644 (file)
@@ -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;
     }
index f3aa81a4dc07198d0ef18afb680522f9589594d5..f59e7c8b6bd053acbddd8a9eeeff199f9ba850d9 100644 (file)
@@ -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<String, String> sqlTables = new HashMap<>();
+    protected final Map<String, String> 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<Integer, String> tableIds = new HashMap<>();
-
-        //
-        for (ItemsVO vo : getItemIDTableNames()) {
-            String t = getTableName(vo.getItemid(), vo.getItemname());
-            sqlTables.put(vo.getItemname(), t);
-            tableIds.put(vo.getItemid(), t);
-        }
+        List<ItemsVO> itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList<ItemsVO>();
+        List<String> itemTables = getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList());
+        List<ItemVO> oldNewTableNames;
 
-        //
-        List<ItemsVO> al = getItemTables();
-
-        String oldName = "";
-        String newName = "";
-        List<ItemVO> 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<Integer, String> itemIdToItemNameMap = new HashMap<>();
 
-        updateItemTableNames(oldNewTablenames);
-        logger.info("JDBC::formatTableNames: Finished updating {} item table names", oldNewTablenames.size());
+            for (ItemsVO vo : itemIdTableNames) {
+                int itemId = vo.getItemId();
+                String itemName = vo.getItemName();
+                itemIdToItemNameMap.put(itemId, itemName);
+            }
 
-        initialized = tmpinit;
-    }
+            oldNewTableNames = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, itemsManageTable);
+        }
 
-    private String getTableName(int rowId, String itemName) {
-        return getTableNamePrefix(itemName) + formatRight(rowId, conf.getTableIdDigitCount());
-    }
+        updateItemTableNames(oldNewTableNames);
+        logger.info("JDBC::formatTableNames: Finished updating {} item table names", oldNewTableNames.size());
 
-    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;
+        initialized = tmpinit;
     }
 
     public Set<PersistenceItemInfo> 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.<PersistenceItemInfo> 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 *
      *****************/
index 20a0fff89ece593069b53540c1ae8ff3ee4b3053..bac25c3e5a11ea2bb54fa6c3e7369f185cb49f9c 100644 (file)
@@ -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 (file)
index 0000000..3fcf9f0
--- /dev/null
@@ -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<ItemVO> prepareMigration(List<String> itemTables, Map<Integer, String> itemIdToItemNameMap,
+            String itemsManageTable) {
+        List<ItemVO> oldNewTableNames = new ArrayList<>();
+        Map<String, Integer> tableNameToItemIdMap = new HashMap<>();
+
+        for (Entry<Integer, String> 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;
+    }
+}
index 31d16a61bdcaa093e1c11ffd90e7f291f80b2aac..cc8411376b4f3f522f54322b5ee573cc7ccb50d6 100644 (file)
                        #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=
                                <option value="false">Disable</option>
                        </options>
                </parameter>
+               <parameter name="tableCaseSensitiveItemNames" type="text">
+                       <label>Tablename Case Sensitive</label>
+                       <description><![CDATA[Enables Tablename generation with case sensitive item names case when "Tablename Realname Generation" is enabled <br>
+                       If true, no suffix is used. (optional, default: disabled -> table names are lower cased with numeric suffix appended).]]></description>
+                       <options>
+                               <option value="true">Enable</option>
+                               <option value="false">Disable</option>
+                       </options>
+               </parameter>
                <parameter name="tableIdDigitCount" type="text">
                        <label>Tablename Suffix ID Count</label>
                        <description><![CDATA[Tablename Suffix ID Count <br>(optional, default: 4 -> 0001-9999). <br>
                </parameter>
                <parameter name="rebuildTableNames" type="text">
                        <label>Tablename Rebuild</label>
-                       <description><![CDATA[Rename existing tables using 'Tablename Realname Generation' and 'Tablename Suffix ID Count', (optional, default: disabled). <br>
+                       <description><![CDATA[Rename existing tables using 'Tablename Prefix String', 'Tablename Realname Generation', 'Tablename Case Sensitive' and
+                       'Tablename Suffix ID Count'. (optional, default: disabled). <br>
                        USE WITH CARE! Deactivate after renaming is done!]]></description>
                        <options>
                                <option value="true">Enable</option>
index 110e4b8d9b9f64b69678d2277f27ec100bf491a5..670c072cca8408e6f0aed24dec6d93c7cf7eef9d 100644 (file)
@@ -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). <br> 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). <br> 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 <br>(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 <br>(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 <br> 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 <br>(optional, default: 4 -> 0001-9999). <br> 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 (file)
index 0000000..257bf97
--- /dev/null
@@ -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<ItemVO> 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<ItemVO> 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<Integer, String> itemIdToItemNameMap = new HashMap<>(2);
+        itemIdToItemNameMap.put(1, "First");
+        itemIdToItemNameMap.put(3, "Third");
+
+        List<String> itemTables = new ArrayList<String>(3);
+        itemTables.add("Item1");
+        itemTables.add("Item2");
+        itemTables.add("Item3");
+
+        List<ItemVO> 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<Integer, String> itemIdToItemNameMap = new HashMap<>(3);
+        itemIdToItemNameMap.put(1, "First");
+        itemIdToItemNameMap.put(2, "Second");
+        itemIdToItemNameMap.put(3, "Third");
+
+        List<String> itemTables = new ArrayList<String>(3);
+        itemTables.add("Item1");
+        itemTables.add("Item002");
+        itemTables.add("third_0003");
+
+        List<ItemVO> 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<Integer, String> itemIdToItemNameMap = new HashMap<>(3);
+        itemIdToItemNameMap.put(1, "First");
+        itemIdToItemNameMap.put(2, "Second");
+        itemIdToItemNameMap.put(3, "Third");
+
+        List<String> itemTables = new ArrayList<String>(3);
+        itemTables.add("Item1");
+        itemTables.add("Item002");
+        itemTables.add("third_0003");
+
+        List<ItemVO> 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<ItemVO> 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<ItemVO> 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<Integer, String> itemIdToItemNameMap = new HashMap<>(2);
+        itemIdToItemNameMap.put(1, "MyItem");
+        itemIdToItemNameMap.put(2, "myItem");
+
+        List<String> itemTables = new ArrayList<String>(2);
+        itemTables.add("MyItem");
+        itemTables.add("myItem");
+
+        List<ItemVO> 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<Integer, String> itemIdToItemNameMap = new HashMap<>(2);
+        itemIdToItemNameMap.put(1, "MyItem");
+        itemIdToItemNameMap.put(2, "myItem");
+
+        List<String> itemTables = new ArrayList<String>(2);
+        itemTables.add("Item1");
+        itemTables.add("Item2");
+
+        List<ItemVO> 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<Integer, String> itemIdToItemNameMap = new HashMap<>(2);
+        itemIdToItemNameMap.put(1, "MyItem");
+        itemIdToItemNameMap.put(2, "myItem");
+
+        List<String> itemTables = new ArrayList<String>(2);
+        itemTables.add("Item1");
+        itemTables.add("Item2");
+
+        List<ItemVO> 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<ItemVO> 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<ItemVO> 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<ItemVO> 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<ItemVO> 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<ItemVO> 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<ItemVO> 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<ItemVO> 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<Integer, String> itemIdToItemNameMap = new HashMap<>(2);
+        itemIdToItemNameMap.put(1, "Item2");
+        itemIdToItemNameMap.put(2, "Item1");
+
+        List<String> itemTables = new ArrayList<String>(2);
+        itemTables.add("Item2");
+        itemTables.add("Item1");
+
+        List<ItemVO> 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<ItemVO> actual = prepareMigrationRealItemNames(itemId, itemName, tableName);
+
+        assertThat(actual.size(), is(0));
+    }
+
+    private List<ItemVO> prepareMigrationNumbered(int itemId, String itemName, String tableName) {
+        return prepareMigrationNumbered(itemId, itemName, tableName, 4);
+    }
+
+    private List<ItemVO> 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<ItemVO> prepareMigrationRealItemNames(int itemId, String itemName, String tableName) {
+        return prepareMigrationRealItemNames(itemId, itemName, tableName, true);
+    }
+
+    private List<ItemVO> 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<ItemVO> prepareMigration(int itemId, String itemName, String tableName) {
+        return prepareMigration(itemId, itemName, tableName, "Item");
+    }
+
+    private List<ItemVO> prepareMigration(int itemId, String itemName, String tableName, String prefix) {
+        Mockito.doReturn(prefix).when(configurationMock).getTableNamePrefix();
+
+        Map<Integer, String> itemIdToItemNameMap = getItemIdToItemNameMap(itemId, itemName);
+        List<String> itemTables = getItemTables(tableName);
+
+        return namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME);
+    }
+
+    private Map<Integer, String> getItemIdToItemNameMap(int itemId, String itemName) {
+        Map<Integer, String> itemIdToItemNameMap = new HashMap<>(1);
+        itemIdToItemNameMap.put(itemId, itemName);
+        return itemIdToItemNameMap;
+    }
+
+    private List<String> getItemTables(String tableName) {
+        List<String> itemTables = new ArrayList<String>(1);
+        itemTables.add(tableName);
+        return itemTables;
+    }
+
+    private void assertTableName(List<ItemVO> actual, String expected) {
+        assertThat(actual.size(), is(1));
+        assertThat(actual.get(0).getNewTableName(), is(expected));
+    }
+}