[jdbc] Add console maintenance commands (#13662)

* Add console command for listing tables
* Query row counts only when needed and while generating output
* Add cleanup command
* Add documentation

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen 2022-11-12 12:30:53 +01:00 committed by GitHub
parent 248ca1830a
commit 159054a99c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 568 additions and 11 deletions

View File

@ -29,6 +29,7 @@ The following databases are currently supported and tested:
- [Database Table Schema](#database-table-schema) - [Database Table Schema](#database-table-schema)
- [Number Precision](#number-precision) - [Number Precision](#number-precision)
- [Rounding results](#rounding-results) - [Rounding results](#rounding-results)
- [Maintenance](#maintenance)
- [For Developers](#for-developers) - [For Developers](#for-developers)
- [Performance Tests](#performance-tests) - [Performance Tests](#performance-tests)
@ -138,6 +139,62 @@ The results of database queries of number items are rounded to three decimal pla
With `numberDecimalcount` decimals can be changed. With `numberDecimalcount` decimals can be changed.
Especially if sql types `DECIMAL` or `NUMERIC` are used for `sqltype.NUMBER`, rounding can be disabled by setting `numberDecimalcount=-1`. Especially if sql types `DECIMAL` or `NUMERIC` are used for `sqltype.NUMBER`, rounding can be disabled by setting `numberDecimalcount=-1`.
### Maintenance
Some maintenance tools are provided as console commands.
#### List Tables
Tables and corresponding items can be listed with the command `jdbc tables list`.
Per default only tables with some kind of problem are listed.
To list all tables, use the command `jdbc tables list all`.
The list contains table name, item name, row count and status, which can be one of:
- **Valid:** Table is consistent.
- **Item missing:** Table has no corresponding item.
- **Table missing:** Referenced table does not exist.
- **Item and table missing:** Referenced table does not exist nor has corresponding item.
- **Orphan table:** Mapping for table does not exist in index.
#### Clean Inconsistent Items
Some issues can be fixed automatically using the command `jdbc clean` (all items having issues) or `jdbc clean <itemName>` (single item).
This cleanup operation will remove items from the index (table `Items`) if the referenced table does not exist.
If the item does not exist, the table will be physically deleted, but only if it's empty.
This precaution is taken because items may have existed previously, and the data might still be valuable.
For example, an item for a lost or repurposed sensor could have been deleted from the system while preserving persisted data.
To skip this check for a single item, use `jdbc clean <itemName> force` with care.
Prior to performing a `jdbc clean` operation, it's recommended to review the result of `jdbc tables list`.
Fixing integrity issues can be useful before performing a migration to another naming scheme.
For example, when migrating to `tableCaseSensitiveItemNames`, an index will no longer exist after the migration:
**Before migration:**
| Table | Row count | Item | Status |
|-------------------|---------: |--------|---------------|
| ActualItem | 0 | | Orphan table |
| TableNotBelonging | 0 | | Orphan table |
| item0077 | 0 | MyItem | Table missing |
**After migration:**
| Table | Row count | Item | Status |
|-------------------|---------: |-------------------|---------------|
| ActualItem | 0 | ActualItem | Valid |
| TableNotBelonging | 0 | TableNotBelonging | Item missing |
This happened:
- `ActualItem` was missing in the index and became valid because it was left untouched, not being a part of the migration. After the migration, it happened to match the name of an existing item, thus it became valid.
- `TableNotBelonging` was also not part of the migration, but since now assumed to match an item, status changed since no item with that name exists.
- `item0077`, being the only correct table name according to previous naming scheme, disappeared from the list since it didn't have a corresponding table, and is now no longer part of any index.
In other words, extracting this information from the index before removing it, can be beneficial in order to understand the issues and possible causes.
### For Developers ### For Developers
* Clearly separated source files for the database-specific part of openHAB logic. * Clearly separated source files for the database-specific part of openHAB logic.

View File

@ -0,0 +1,45 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This class represents a checked item/table relation.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class ItemTableCheckEntry {
private String itemName;
private String tableName;
private ItemTableCheckEntryStatus status;
public ItemTableCheckEntry(String itemName, String tableName, ItemTableCheckEntryStatus status) {
this.itemName = itemName;
this.tableName = tableName;
this.status = status;
}
public String getItemName() {
return itemName;
}
public String getTableName() {
return tableName;
}
public ItemTableCheckEntryStatus getStatus() {
return status;
}
}

View File

@ -0,0 +1,69 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This class represents status for an {@link ItemTableCheckEntry}.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public enum ItemTableCheckEntryStatus {
/**
* Table is consistent.
*/
VALID {
@Override
public String toString() {
return "Valid";
}
},
/**
* Table has no corresponding item.
*/
ITEM_MISSING {
@Override
public String toString() {
return "Item missing";
}
},
/**
* Referenced table does not exist.
*/
TABLE_MISSING {
@Override
public String toString() {
return "Table missing";
}
},
/**
* Referenced table does not exist nor has corresponding item.
*/
ITEM_AND_TABLE_MISSING {
@Override
public String toString() {
return "Item and table missing";
}
},
/**
* Mapping for table does not exist in index.
*/
ORPHAN_TABLE {
@Override
public String toString() {
return "Orphan table";
}
}
}

View File

@ -0,0 +1,191 @@
/**
* 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.console;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.ConsoleCommandCompleter;
import org.openhab.core.io.console.StringsCompleter;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
import org.openhab.core.persistence.PersistenceService;
import org.openhab.core.persistence.PersistenceServiceRegistry;
import org.openhab.persistence.jdbc.ItemTableCheckEntry;
import org.openhab.persistence.jdbc.ItemTableCheckEntryStatus;
import org.openhab.persistence.jdbc.internal.JdbcPersistenceService;
import org.openhab.persistence.jdbc.internal.JdbcPersistenceServiceConstants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link JdbcCommandExtension} is responsible for handling console commands
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
@Component(service = ConsoleCommandExtension.class)
public class JdbcCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter {
private static final String CMD_TABLES = "tables";
private static final String SUBCMD_TABLES_LIST = "list";
private static final String SUBCMD_TABLES_CLEAN = "clean";
private static final String PARAMETER_ALL = "all";
private static final String PARAMETER_FORCE = "force";
private static final StringsCompleter CMD_COMPLETER = new StringsCompleter(List.of(CMD_TABLES), false);
private static final StringsCompleter SUBCMD_TABLES_COMPLETER = new StringsCompleter(
List.of(SUBCMD_TABLES_LIST, SUBCMD_TABLES_CLEAN), false);
private final PersistenceServiceRegistry persistenceServiceRegistry;
@Activate
public JdbcCommandExtension(final @Reference PersistenceServiceRegistry persistenceServiceRegistry) {
super(JdbcPersistenceServiceConstants.SERVICE_ID, "Interact with the JDBC persistence service.");
this.persistenceServiceRegistry = persistenceServiceRegistry;
}
@Override
public void execute(String[] args, Console console) {
if (args.length < 2 || args.length > 4 || !CMD_TABLES.equals(args[0])) {
printUsage(console);
return;
}
JdbcPersistenceService persistenceService = getPersistenceService();
if (persistenceService == null) {
return;
}
if (SUBCMD_TABLES_LIST.equalsIgnoreCase(args[1])) {
listTables(persistenceService, console, args.length == 3 && PARAMETER_ALL.equalsIgnoreCase(args[2]));
return;
} else if (SUBCMD_TABLES_CLEAN.equalsIgnoreCase(args[1])) {
if (args.length == 3) {
cleanupItem(persistenceService, console, args[2], false);
return;
} else if (args.length == 4 && PARAMETER_FORCE.equalsIgnoreCase(args[3])) {
cleanupItem(persistenceService, console, args[2], true);
return;
} else {
cleanupTables(persistenceService, console);
return;
}
}
printUsage(console);
}
private @Nullable JdbcPersistenceService getPersistenceService() {
for (PersistenceService persistenceService : persistenceServiceRegistry.getAll()) {
if (persistenceService instanceof JdbcPersistenceService) {
return (JdbcPersistenceService) persistenceService;
}
}
return null;
}
private void listTables(JdbcPersistenceService persistenceService, Console console, Boolean all) {
List<ItemTableCheckEntry> entries = persistenceService.getCheckedEntries();
if (!all) {
entries.removeIf(t -> t.getStatus() == ItemTableCheckEntryStatus.VALID);
}
entries.sort(Comparator.comparing(ItemTableCheckEntry::getTableName));
int itemNameMaxLength = Math
.max(entries.stream().map(t -> t.getItemName().length()).max(Integer::compare).get(), 4);
int tableNameMaxLength = Math
.max(entries.stream().map(t -> t.getTableName().length()).max(Integer::compare).get(), 5);
int statusMaxLength = Stream.of(ItemTableCheckEntryStatus.values()).map(t -> t.toString().length())
.max(Integer::compare).get();
console.println(String.format(
"%1$-" + (tableNameMaxLength + 2) + "sRow Count %2$-" + (itemNameMaxLength + 2) + "s%3$s", "Table",
"Item", "Status"));
console.println("-".repeat(tableNameMaxLength) + " " + "--------- " + "-".repeat(itemNameMaxLength) + " "
+ "-".repeat(statusMaxLength));
for (ItemTableCheckEntry entry : entries) {
String tableName = entry.getTableName();
ItemTableCheckEntryStatus status = entry.getStatus();
long rowCount = status == ItemTableCheckEntryStatus.VALID
|| status == ItemTableCheckEntryStatus.ITEM_MISSING ? persistenceService.getRowCount(tableName) : 0;
console.println(String.format(
"%1$-" + (tableNameMaxLength + 2) + "s%2$9d %3$-" + (itemNameMaxLength + 2) + "s%4$s", tableName,
rowCount, entry.getItemName(), status));
}
}
private void cleanupTables(JdbcPersistenceService persistenceService, Console console) {
console.println("Cleaning up all inconsistent items...");
List<ItemTableCheckEntry> entries = persistenceService.getCheckedEntries();
entries.removeIf(t -> t.getStatus() == ItemTableCheckEntryStatus.VALID || t.getItemName().isEmpty());
for (ItemTableCheckEntry entry : entries) {
console.print(entry.getItemName() + " -> ");
if (persistenceService.cleanupItem(entry)) {
console.println("done.");
} else {
console.println("skipped/failed.");
}
}
}
private void cleanupItem(JdbcPersistenceService persistenceService, Console console, String itemName,
boolean force) {
console.print("Cleaning up item " + itemName + "... ");
if (persistenceService.cleanupItem(itemName, force)) {
console.println("done.");
} else {
console.println("skipped/failed.");
}
}
@Override
public List<String> getUsages() {
return Arrays.asList(
buildCommandUsage(CMD_TABLES + " " + SUBCMD_TABLES_LIST + " [" + PARAMETER_ALL + "]",
"list tables (all = include valid)"),
buildCommandUsage(
CMD_TABLES + " " + SUBCMD_TABLES_CLEAN + " [<itemName>]" + " [" + PARAMETER_FORCE + "]",
"clean inconsistent items (remove from index and drop tables)"));
}
@Override
public @Nullable ConsoleCommandCompleter getCompleter() {
return this;
}
@Override
public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
if (cursorArgumentIndex <= 0) {
return CMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
} else if (cursorArgumentIndex == 1) {
if (CMD_TABLES.equalsIgnoreCase(args[0])) {
return SUBCMD_TABLES_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
}
} else if (cursorArgumentIndex == 2) {
if (CMD_TABLES.equalsIgnoreCase(args[0])) {
if (SUBCMD_TABLES_CLEAN.equalsIgnoreCase(args[1])) {
JdbcPersistenceService persistenceService = getPersistenceService();
if (persistenceService != null) {
return new StringsCompleter(persistenceService.getItemNames(), true).complete(args,
cursorArgumentIndex, cursorPosition, candidates);
}
} else if (SUBCMD_TABLES_LIST.equalsIgnoreCase(args[1])) {
new StringsCompleter(List.of(PARAMETER_ALL), false).complete(args, cursorArgumentIndex,
cursorPosition, candidates);
}
}
}
return false;
}
}

View File

@ -85,11 +85,13 @@ public class JdbcBaseDAO {
protected String sqlCreateNewEntryInItemsTable = "INSERT INTO #itemsManageTable# (ItemName) VALUES ('#itemname#')"; 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 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 sqlDropItemsTableIfExists = "DROP TABLE IF EXISTS #itemsManageTable#";
protected String sqlDeleteItemsEntry = "DELETE FROM items WHERE ItemName=#itemname#"; protected String sqlDropTable = "DROP TABLE #tableName#";
protected String sqlDeleteItemsEntry = "DELETE FROM #itemsManageTable# 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 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 sqlCreateItemTable = "CREATE TABLE IF NOT EXISTS #tableName# (time #tablePrimaryKey# NOT NULL, value #dbType#, PRIMARY KEY(time))";
protected String sqlInsertItemValue = "INSERT INTO #tableName# (TIME, VALUE) VALUES( #tablePrimaryValue#, ? ) ON DUPLICATE KEY UPDATE VALUE= ?"; protected String sqlInsertItemValue = "INSERT INTO #tableName# (TIME, VALUE) VALUES( #tablePrimaryValue#, ? ) ON DUPLICATE KEY UPDATE VALUE= ?";
protected String sqlGetRowCount = "SELECT COUNT(*) FROM #tableName#";
/******** /********
* INIT * * INIT *
@ -264,6 +266,14 @@ public class JdbcBaseDAO {
return Objects.nonNull(result); return Objects.nonNull(result);
} }
public boolean doIfTableExists(String tableName) {
String sql = StringUtilsExt.replaceArrayMerge(sqlIfTableExists, new String[] { "#searchTable#" },
new String[] { tableName });
logger.debug("JDBC::doIfTableExists sql={}", sql);
final @Nullable String result = Yank.queryScalar(sql, String.class, null);
return Objects.nonNull(result);
}
public Long doCreateNewEntryInItemsTable(ItemsVO vo) { public Long doCreateNewEntryInItemsTable(ItemsVO vo) {
String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable, String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable,
new String[] { "#itemsManageTable#", "#itemname#" }, new String[] { "#itemsManageTable#", "#itemname#" },
@ -289,9 +299,17 @@ public class JdbcBaseDAO {
return vo; return vo;
} }
public void doDropTable(String tableName) {
String sql = StringUtilsExt.replaceArrayMerge(sqlDropTable, new String[] { "#tableName#" },
new String[] { tableName });
logger.debug("JDBC::doDropTable sql={}", sql);
Yank.execute(sql, null);
}
public void doDeleteItemsEntry(ItemsVO vo) { public void doDeleteItemsEntry(ItemsVO vo) {
String sql = StringUtilsExt.replaceArrayMerge(sqlDeleteItemsEntry, new String[] { "#itemname#" }, String sql = StringUtilsExt.replaceArrayMerge(sqlDeleteItemsEntry,
new String[] { vo.getItemName() }); new String[] { "#itemsManageTable#", "#itemname#" },
new String[] { vo.getItemsManageTable(), vo.getItemName() });
logger.debug("JDBC::doDeleteItemsEntry sql={}", sql); logger.debug("JDBC::doDeleteItemsEntry sql={}", sql);
Yank.execute(sql, null); Yank.execute(sql, null);
} }
@ -373,6 +391,14 @@ public class JdbcBaseDAO {
Yank.execute(sql, null); Yank.execute(sql, null);
} }
public long doGetRowCount(String tableName) {
final String sql = StringUtilsExt.replaceArrayMerge(sqlGetRowCount, new String[] { "#tableName#" },
new String[] { tableName });
logger.debug("JDBC::doGetRowCount sql={}", sql);
final @Nullable Long result = Yank.queryScalar(sql, Long.class, null);
return Objects.requireNonNullElse(result, 0L);
}
/************* /*************
* Providers * * Providers *
*************/ *************/

View File

@ -106,6 +106,14 @@ public class JdbcMapper {
return res; return res;
} }
public boolean ifTableExists(String tableName) {
logger.debug("JDBC::ifTableExists");
long timerStart = System.currentTimeMillis();
boolean res = conf.getDBDAO().doIfTableExists(tableName);
logTime("doIfTableExists", timerStart, System.currentTimeMillis());
return res;
}
public ItemsVO createNewEntryInItemsTable(ItemsVO vo) { public ItemsVO createNewEntryInItemsTable(ItemsVO vo) {
logger.debug("JDBC::createNewEntryInItemsTable"); logger.debug("JDBC::createNewEntryInItemsTable");
long timerStart = System.currentTimeMillis(); long timerStart = System.currentTimeMillis();
@ -131,6 +139,13 @@ public class JdbcMapper {
return true; return true;
} }
public void dropTable(String tableName) {
logger.debug("JDBC::dropTable");
long timerStart = System.currentTimeMillis();
conf.getDBDAO().doDropTable(tableName);
logTime("doDropTable", timerStart, System.currentTimeMillis());
}
public ItemsVO deleteItemsEntry(ItemsVO vo) { public ItemsVO deleteItemsEntry(ItemsVO vo) {
logger.debug("JDBC::deleteItemsEntry"); logger.debug("JDBC::deleteItemsEntry");
long timerStart = System.currentTimeMillis(); long timerStart = System.currentTimeMillis();
@ -189,6 +204,10 @@ public class JdbcMapper {
return item; return item;
} }
public long getRowCount(String tableName) {
return conf.getDBDAO().doGetRowCount(tableName);
}
public List<HistoricItem> getHistItemFilterQuery(FilterCriteria filter, int numberDecimalcount, String table, public List<HistoricItem> getHistItemFilterQuery(FilterCriteria filter, int numberDecimalcount, String table,
Item item) { Item item) {
logger.debug( logger.debug(
@ -350,7 +369,7 @@ public class JdbcMapper {
} }
List<ItemsVO> itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList<ItemsVO>(); List<ItemsVO> itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList<ItemsVO>();
List<String> itemTables = getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList()); var itemTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toList());
List<ItemVO> oldNewTableNames; List<ItemVO> oldNewTableNames;
if (itemIdTableNames.isEmpty()) { if (itemIdTableNames.isEmpty()) {

View File

@ -13,11 +13,15 @@
package org.openhab.persistence.jdbc.internal; package org.openhab.persistence.jdbc.internal;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -36,6 +40,9 @@ import org.openhab.core.persistence.QueryablePersistenceService;
import org.openhab.core.persistence.strategy.PersistenceStrategy; import org.openhab.core.persistence.strategy.PersistenceStrategy;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType; import org.openhab.core.types.UnDefType;
import org.openhab.persistence.jdbc.ItemTableCheckEntry;
import org.openhab.persistence.jdbc.ItemTableCheckEntryStatus;
import org.openhab.persistence.jdbc.dto.ItemsVO;
import org.osgi.framework.BundleContext; import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants; import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
@ -55,13 +62,9 @@ import org.slf4j.LoggerFactory;
@Component(service = { PersistenceService.class, @Component(service = { PersistenceService.class,
QueryablePersistenceService.class }, configurationPid = "org.openhab.jdbc", // QueryablePersistenceService.class }, configurationPid = "org.openhab.jdbc", //
property = Constants.SERVICE_PID + "=org.openhab.jdbc") property = Constants.SERVICE_PID + "=org.openhab.jdbc")
@ConfigurableService(category = "persistence", label = "JDBC Persistence Service", description_uri = JdbcPersistenceService.CONFIG_URI) @ConfigurableService(category = "persistence", label = "JDBC Persistence Service", description_uri = JdbcPersistenceServiceConstants.CONFIG_URI)
public class JdbcPersistenceService extends JdbcMapper implements ModifiablePersistenceService { public class JdbcPersistenceService extends JdbcMapper implements ModifiablePersistenceService {
private static final String SERVICE_ID = "jdbc";
private static final String SERVICE_LABEL = "JDBC";
protected static final String CONFIG_URI = "persistence:jdbc";
private final Logger logger = LoggerFactory.getLogger(JdbcPersistenceService.class); private final Logger logger = LoggerFactory.getLogger(JdbcPersistenceService.class);
private final ItemRegistry itemRegistry; private final ItemRegistry itemRegistry;
@ -116,12 +119,12 @@ public class JdbcPersistenceService extends JdbcMapper implements ModifiablePers
@Override @Override
public String getId() { public String getId() {
logger.debug("JDBC::getName: returning name 'jdbc' for queryable persistence service."); logger.debug("JDBC::getName: returning name 'jdbc' for queryable persistence service.");
return SERVICE_ID; return JdbcPersistenceServiceConstants.SERVICE_ID;
} }
@Override @Override
public String getLabel(@Nullable Locale locale) { public String getLabel(@Nullable Locale locale) {
return SERVICE_LABEL; return JdbcPersistenceServiceConstants.SERVICE_LABEL;
} }
@Override @Override
@ -275,4 +278,122 @@ public class JdbcPersistenceService extends JdbcMapper implements ModifiablePers
return result; return result;
} }
/**
* Get a list of names of persisted items.
*/
public Collection<String> getItemNames() {
return itemNameToTableNameMap.keySet();
}
/**
* Get a list of all items with corresponding tables and an {@link ItemTableCheckEntryStatus} indicating
* its condition.
*
* @return list of {@link ItemTableCheckEntry}
*/
public List<ItemTableCheckEntry> getCheckedEntries() {
List<ItemTableCheckEntry> entries = new ArrayList<>();
if (!checkDBAccessability()) {
logger.warn("JDBC::getCheckedEntries: database not connected");
return entries;
}
var orphanTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toSet());
for (Entry<String, String> entry : itemNameToTableNameMap.entrySet()) {
String itemName = entry.getKey();
String tableName = entry.getValue();
entries.add(getCheckedEntry(itemName, tableName, orphanTables.contains(tableName)));
orphanTables.remove(tableName);
}
for (String orphanTable : orphanTables) {
entries.add(new ItemTableCheckEntry("", orphanTable, ItemTableCheckEntryStatus.ORPHAN_TABLE));
}
return entries;
}
private ItemTableCheckEntry getCheckedEntry(String itemName, String tableName, boolean tableExists) {
boolean itemExists;
try {
itemRegistry.getItem(itemName);
itemExists = true;
} catch (ItemNotFoundException e) {
itemExists = false;
}
ItemTableCheckEntryStatus status;
if (!tableExists) {
if (itemExists) {
status = ItemTableCheckEntryStatus.TABLE_MISSING;
} else {
status = ItemTableCheckEntryStatus.ITEM_AND_TABLE_MISSING;
}
} else if (itemExists) {
status = ItemTableCheckEntryStatus.VALID;
} else {
status = ItemTableCheckEntryStatus.ITEM_MISSING;
}
return new ItemTableCheckEntry(itemName, tableName, status);
}
/**
* Clean up inconsistent item: Remove from index and drop table.
* Tables with any rows are skipped, unless force is set.
*
* @param itemName Name of item to clean
* @param force If true, non-empty tables will be dropped too
* @return true if item was cleaned up
*/
public boolean cleanupItem(String itemName, boolean force) {
String tableName = itemNameToTableNameMap.get(itemName);
if (tableName == null) {
return false;
}
ItemTableCheckEntry entry = getCheckedEntry(itemName, tableName, ifTableExists(tableName));
return cleanupItem(entry, force);
}
/**
* Clean up inconsistent item: Remove from index and drop table.
* Tables with any rows are skipped.
*
* @param entry
* @return true if item was cleaned up
*/
public boolean cleanupItem(ItemTableCheckEntry entry) {
return cleanupItem(entry, false);
}
private boolean cleanupItem(ItemTableCheckEntry entry, boolean force) {
if (!checkDBAccessability()) {
logger.warn("JDBC::cleanupItem: database not connected");
return false;
}
ItemTableCheckEntryStatus status = entry.getStatus();
String tableName = entry.getTableName();
switch (status) {
case ITEM_MISSING:
if (!force && getRowCount(tableName) > 0) {
return false;
}
dropTable(tableName);
// Fall through to remove from index.
case TABLE_MISSING:
case ITEM_AND_TABLE_MISSING:
if (!conf.getTableUseRealCaseSensitiveItemNames()) {
ItemsVO itemsVo = new ItemsVO();
itemsVo.setItemName(entry.getItemName());
deleteItemsEntry(itemsVo);
}
itemNameToTableNameMap.remove(entry.getItemName());
return true;
case ORPHAN_TABLE:
case VALID:
default:
// Nothing to clean.
return false;
}
}
} }

View File

@ -0,0 +1,29 @@
/**
* 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link JdbcPersistenceServiceConstants} class defines common constants, which are
* used across the whole persistence service.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class JdbcPersistenceServiceConstants {
public static final String SERVICE_ID = "jdbc";
public static final String SERVICE_LABEL = "JDBC";
public static final String CONFIG_URI = "persistence:jdbc";
}