[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 <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen 2022-11-05 10:41:31 +01:00 committed by GitHub
parent f880ca91f9
commit 70abb5d1f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 779 additions and 172 deletions

View 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.

View 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;
}

View 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);
}

View 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);
}

View 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);
}

View 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();
}

View 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;
}

View 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<>();
List<ItemsVO> itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList<ItemsVO>();
List<String> itemTables = getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList());
List<ItemVO> oldNewTableNames;
//
for (ItemsVO vo : getItemIDTableNames()) {
String t = getTableName(vo.getItemid(), vo.getItemname());
sqlTables.put(vo.getItemname(), t);
tableIds.put(vo.getItemid(), t);
}
//
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<>();
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<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 *
*****************/

View 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;

View File

@ -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;
}
}

View File

@ -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 @@
<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>
@ -172,7 +186,8 @@
</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>

View 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

View File

@ -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));
}
}