diff --git a/bundles/org.openhab.persistence.jdbc/README.md b/bundles/org.openhab.persistence.jdbc/README.md index bc20c206a..bec3af3fd 100644 --- a/bundles/org.openhab.persistence.jdbc/README.md +++ b/bundles/org.openhab.persistence.jdbc/README.md @@ -29,6 +29,7 @@ The following databases are currently supported and tested: - [Database Table Schema](#database-table-schema) - [Number Precision](#number-precision) - [Rounding results](#rounding-results) + - [Maintenance](#maintenance) - [For Developers](#for-developers) - [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. 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 ` (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 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 * Clearly separated source files for the database-specific part of openHAB logic. diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntry.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntry.java new file mode 100644 index 000000000..e644138ae --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntry.java @@ -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; + } +} diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntryStatus.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntryStatus.java new file mode 100644 index 000000000..3553aee29 --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntryStatus.java @@ -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"; + } + } +} diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/console/JdbcCommandExtension.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/console/JdbcCommandExtension.java new file mode 100644 index 000000000..0bcab36a2 --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/console/JdbcCommandExtension.java @@ -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 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 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 getUsages() { + return Arrays.asList( + buildCommandUsage(CMD_TABLES + " " + SUBCMD_TABLES_LIST + " [" + PARAMETER_ALL + "]", + "list tables (all = include valid)"), + buildCommandUsage( + CMD_TABLES + " " + SUBCMD_TABLES_CLEAN + " []" + " [" + 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 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; + } +} diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java index 6bc19dc7a..393635d96 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java @@ -85,11 +85,13 @@ public class JdbcBaseDAO { 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 sqlDropTable = "DROP TABLE #tableName#"; + protected String sqlDeleteItemsEntry = "DELETE FROM #itemsManageTable# WHERE ItemName='#itemname#'"; protected String sqlGetItemIDTableNames = "SELECT ItemId, ItemName FROM #itemsManageTable#"; protected String sqlGetItemTables = "SELECT table_name FROM information_schema.tables WHERE table_type='BASE TABLE' AND table_schema='#jdbcUriDatabaseName#' AND NOT table_name='#itemsManageTable#'"; protected String 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 sqlGetRowCount = "SELECT COUNT(*) FROM #tableName#"; /******** * INIT * @@ -264,6 +266,14 @@ public class JdbcBaseDAO { 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) { String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable, new String[] { "#itemsManageTable#", "#itemname#" }, @@ -289,9 +299,17 @@ public class JdbcBaseDAO { 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) { - String sql = StringUtilsExt.replaceArrayMerge(sqlDeleteItemsEntry, new String[] { "#itemname#" }, - new String[] { vo.getItemName() }); + String sql = StringUtilsExt.replaceArrayMerge(sqlDeleteItemsEntry, + new String[] { "#itemsManageTable#", "#itemname#" }, + new String[] { vo.getItemsManageTable(), vo.getItemName() }); logger.debug("JDBC::doDeleteItemsEntry sql={}", sql); Yank.execute(sql, null); } @@ -373,6 +391,14 @@ public class JdbcBaseDAO { 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 * *************/ diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java index f59e7c8b6..c956e706e 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java @@ -106,6 +106,14 @@ public class JdbcMapper { 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) { logger.debug("JDBC::createNewEntryInItemsTable"); long timerStart = System.currentTimeMillis(); @@ -131,6 +139,13 @@ public class JdbcMapper { 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) { logger.debug("JDBC::deleteItemsEntry"); long timerStart = System.currentTimeMillis(); @@ -189,6 +204,10 @@ public class JdbcMapper { return item; } + public long getRowCount(String tableName) { + return conf.getDBDAO().doGetRowCount(tableName); + } + public List getHistItemFilterQuery(FilterCriteria filter, int numberDecimalcount, String table, Item item) { logger.debug( @@ -350,7 +369,7 @@ public class JdbcMapper { } List itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList(); - List itemTables = getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList()); + var itemTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toList()); List oldNewTableNames; if (itemIdTableNames.isEmpty()) { diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java index bac25c3e5..d267ad097 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java @@ -13,11 +13,15 @@ package org.openhab.persistence.jdbc.internal; import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; 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.types.State; 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.Constants; import org.osgi.service.component.annotations.Activate; @@ -55,13 +62,9 @@ import org.slf4j.LoggerFactory; @Component(service = { PersistenceService.class, QueryablePersistenceService.class }, configurationPid = "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 { - 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 ItemRegistry itemRegistry; @@ -116,12 +119,12 @@ public class JdbcPersistenceService extends JdbcMapper implements ModifiablePers @Override public String getId() { logger.debug("JDBC::getName: returning name 'jdbc' for queryable persistence service."); - return SERVICE_ID; + return JdbcPersistenceServiceConstants.SERVICE_ID; } @Override public String getLabel(@Nullable Locale locale) { - return SERVICE_LABEL; + return JdbcPersistenceServiceConstants.SERVICE_LABEL; } @Override @@ -275,4 +278,122 @@ public class JdbcPersistenceService extends JdbcMapper implements ModifiablePers return result; } + + /** + * Get a list of names of persisted items. + */ + public Collection 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 getCheckedEntries() { + List 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 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; + } + } } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceServiceConstants.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceServiceConstants.java new file mode 100644 index 000000000..53d4ee5cf --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceServiceConstants.java @@ -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"; +}