2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.persistence.jdbc.internal.db;
15 import java.time.ZoneId;
16 import java.time.ZonedDateTime;
17 import java.util.List;
18 import java.util.Objects;
19 import java.util.stream.Collectors;
21 import javax.measure.Quantity;
22 import javax.measure.Unit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.knowm.yank.Yank;
27 import org.knowm.yank.exceptions.YankSQLException;
28 import org.openhab.core.items.Item;
29 import org.openhab.core.library.items.NumberItem;
30 import org.openhab.core.persistence.FilterCriteria;
31 import org.openhab.core.persistence.FilterCriteria.Ordering;
32 import org.openhab.core.persistence.HistoricItem;
33 import org.openhab.core.types.State;
34 import org.openhab.persistence.jdbc.internal.dto.ItemVO;
35 import org.openhab.persistence.jdbc.internal.dto.ItemsVO;
36 import org.openhab.persistence.jdbc.internal.dto.JdbcHistoricItem;
37 import org.openhab.persistence.jdbc.internal.exceptions.JdbcSQLException;
38 import org.openhab.persistence.jdbc.internal.utils.StringUtilsExt;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * Extended Database Configuration class. Class represents
44 * the extended database-specific configuration. Overrides and supplements the
45 * default settings from JdbcBaseDAO. Enter only the differences to JdbcBaseDAO here.
47 * @author Helmut Lehmeyer - Initial contribution
50 public class JdbcDerbyDAO extends JdbcBaseDAO {
51 private static final String DRIVER_CLASS_NAME = org.apache.derby.jdbc.EmbeddedDriver.class.getName();
52 @SuppressWarnings("unused")
53 private static final String DATA_SOURCE_CLASS_NAME = org.apache.derby.jdbc.EmbeddedDataSource.class.getName();
55 private final Logger logger = LoggerFactory.getLogger(JdbcDerbyDAO.class);
60 public JdbcDerbyDAO() {
66 private void initSqlQueries() {
67 logger.debug("JDBC::initSqlQueries: '{}'", this.getClass().getSimpleName());
68 sqlPingDB = "values 1";
69 sqlGetDB = "VALUES SYSCS_UTIL.SYSCS_GET_DATABASE_PROPERTY( 'DataDictionaryVersion' )"; // returns version
70 sqlIfTableExists = "SELECT * FROM SYS.SYSTABLES WHERE TABLENAME='#searchTable#'";
71 sqlCreateItemsTableIfNot = "CREATE TABLE #itemsManageTable# ( ItemId INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1), #colname# #coltype# NOT NULL)";
72 sqlCreateItemTable = "CREATE TABLE #tableName# (time #tablePrimaryKey# NOT NULL, value #dbType#, PRIMARY KEY(time))";
73 // Prevent error against duplicate time value (seldom): No powerful Merge found:
74 // http://www.codeproject.com/Questions/162627/how-to-insert-new-record-in-my-table-if-not-exists
75 sqlInsertItemValue = "INSERT INTO #tableName# (TIME, VALUE) VALUES( #tablePrimaryValue#, CAST( ? as #dbType#) )";
76 sqlAlterTableColumn = "ALTER TABLE #tableName# ALTER COLUMN #columnName# SET DATA TYPE #columnType#";
79 private void initSqlTypes() {
80 sqlTypes.put("DATETIMEITEM", "TIMESTAMP");
81 sqlTypes.put("DIMMERITEM", "SMALLINT");
82 sqlTypes.put("IMAGEITEM", "VARCHAR(32000)");
83 sqlTypes.put("ROLLERSHUTTERITEM", "SMALLINT");
84 sqlTypes.put("STRINGITEM", "VARCHAR(32000)");
85 sqlTypes.put("tablePrimaryValue", "CURRENT_TIMESTAMP");
86 logger.debug("JDBC::initSqlTypes: Initialized the type array sqlTypes={}", sqlTypes.values());
90 * INFO: https://github.com/brettwooldridge/HikariCP
92 private void initDbProps() {
93 // Properties for HikariCP
94 // Use driverClassName
95 databaseProps.setProperty("driverClassName", DRIVER_CLASS_NAME);
96 // OR dataSourceClassName
97 // databaseProps.setProperty("dataSourceClassName", DATA_SOURCE_CLASS_NAME);
98 databaseProps.setProperty("maximumPoolSize", "1");
99 databaseProps.setProperty("minimumIdle", "1");
103 public void initAfterFirstDbConnection() {
104 logger.debug("JDBC::initAfterFirstDbConnection: Initializing step, after db is connected.");
105 // Initialize sqlTypes, depending on DB version for example
106 // derby does not like this... dbMeta = new DbMetaData();// get DB information
113 public @Nullable Integer doPingDB() throws JdbcSQLException {
115 return Yank.queryScalar(sqlPingDB, Integer.class, null);
116 } catch (YankSQLException e) {
117 throw new JdbcSQLException(e);
122 public boolean doIfTableExists(ItemsVO vo) throws JdbcSQLException {
123 String sql = StringUtilsExt.replaceArrayMerge(sqlIfTableExists, new String[] { "#searchTable#" },
124 new String[] { vo.getItemsManageTable().toUpperCase() });
125 logger.debug("JDBC::doIfTableExists sql={}", sql);
127 final @Nullable String result = Yank.queryScalar(sql, String.class, null);
128 return Objects.nonNull(result);
129 } catch (YankSQLException e) {
130 throw new JdbcSQLException(e);
135 public Long doCreateNewEntryInItemsTable(ItemsVO vo) throws JdbcSQLException {
136 String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable,
137 new String[] { "#itemsManageTable#", "#itemname#" },
138 new String[] { vo.getItemsManageTable().toUpperCase(), vo.getItemName() });
139 logger.debug("JDBC::doCreateNewEntryInItemsTable sql={}", sql);
141 return Yank.insert(sql, null);
142 } catch (YankSQLException e) {
143 throw new JdbcSQLException(e);
148 public ItemsVO doCreateItemsTableIfNot(ItemsVO vo) throws JdbcSQLException {
149 boolean tableExists = doIfTableExists(vo);
151 String sql = StringUtilsExt.replaceArrayMerge(sqlCreateItemsTableIfNot,
152 new String[] { "#itemsManageTable#", "#colname#", "#coltype#" },
153 new String[] { vo.getItemsManageTable().toUpperCase(), vo.getColname(), vo.getColtype() });
154 logger.debug("JDBC::doCreateItemsTableIfNot tableExists={} therefore sql={}", tableExists, sql);
156 Yank.execute(sql, null);
157 } catch (YankSQLException e) {
158 throw new JdbcSQLException(e);
161 logger.debug("JDBC::doCreateItemsTableIfNot tableExists={}, did not CREATE TABLE", tableExists);
170 public void doCreateItemTable(ItemVO vo) throws JdbcSQLException {
171 String sql = StringUtilsExt.replaceArrayMerge(sqlCreateItemTable,
172 new String[] { "#tableName#", "#dbType#", "#tablePrimaryKey#" },
173 new String[] { vo.getTableName(), vo.getDbType(), sqlTypes.get("tablePrimaryKey") });
175 Yank.execute(sql, null);
176 } catch (YankSQLException e) {
177 throw new JdbcSQLException(e);
182 public void doStoreItemValue(Item item, State itemState, ItemVO vo) throws JdbcSQLException {
183 ItemVO storedVO = storeItemValueProvider(item, itemState, vo);
184 String sql = StringUtilsExt.replaceArrayMerge(sqlInsertItemValue,
185 new String[] { "#tableName#", "#dbType#", "#tablePrimaryValue#" },
186 new String[] { storedVO.getTableName().toUpperCase(), storedVO.getDbType(),
187 sqlTypes.get("tablePrimaryValue") });
188 Object[] params = { storedVO.getValue() };
189 logger.debug("JDBC::doStoreItemValue sql={} value='{}'", sql, storedVO.getValue());
191 Yank.execute(sql, params);
192 } catch (YankSQLException e) {
193 throw new JdbcSQLException(e);
198 public void doStoreItemValue(Item item, State itemState, ItemVO vo, ZonedDateTime date) throws JdbcSQLException {
199 ItemVO storedVO = storeItemValueProvider(item, itemState, vo);
200 String sql = StringUtilsExt.replaceArrayMerge(sqlInsertItemValue,
201 new String[] { "#tableName#", "#dbType#", "#tablePrimaryValue#" },
202 new String[] { storedVO.getTableName().toUpperCase(), storedVO.getDbType(), "?" });
203 java.sql.Timestamp timestamp = new java.sql.Timestamp(date.toInstant().toEpochMilli());
204 Object[] params = { timestamp, storedVO.getValue() };
205 logger.debug("JDBC::doStoreItemValue sql={} timestamp={} value='{}'", sql, timestamp, storedVO.getValue());
207 Yank.execute(sql, params);
208 } catch (YankSQLException e) {
209 throw new JdbcSQLException(e);
214 public List<HistoricItem> doGetHistItemFilterQuery(Item item, FilterCriteria filter, int numberDecimalcount,
215 String table, String name, ZoneId timeZone) throws JdbcSQLException {
216 String sql = histItemFilterQueryProvider(filter, numberDecimalcount, table, name, timeZone);
219 m = Yank.queryObjectArrays(sql, null);
220 } catch (YankSQLException e) {
221 throw new JdbcSQLException(e);
223 logger.debug("JDBC::doGetHistItemFilterQuery got Array length={}", m.size());
224 // we already retrieve the unit here once as it is a very costly operation
225 String itemName = item.getName();
226 Unit<? extends Quantity<?>> unit = item instanceof NumberItem ni ? ni.getUnit() : null;
227 return m.stream().map(o -> {
228 logger.debug("JDBC::doGetHistItemFilterQuery 0='{}' 1='{}'", o[0], o[1]);
229 return new JdbcHistoricItem(itemName, objectAsState(item, unit, o[1]), objectAsZonedDateTime(o[0]));
230 }).collect(Collectors.<HistoricItem> toList());
233 /****************************
234 * SQL generation Providers *
235 ****************************/
238 protected String histItemFilterQueryProvider(FilterCriteria filter, int numberDecimalcount, String table,
239 String simpleName, ZoneId timeZone) {
241 "JDBC::getHistItemFilterQueryProvider filter = {}, numberDecimalcount = {}, table = {}, simpleName = {}",
242 StringUtilsExt.filterToString(filter), numberDecimalcount, table, simpleName);
244 String filterString = "";
245 ZonedDateTime beginDate = filter.getBeginDate();
246 if (beginDate != null) {
247 filterString += filterString.isEmpty() ? " WHERE" : " AND";
248 filterString += " TIME>='" + JDBC_DATE_FORMAT.format(beginDate.withZoneSameInstant(timeZone)) + "'";
250 ZonedDateTime endDate = filter.getEndDate();
251 if (endDate != null) {
252 filterString += filterString.isEmpty() ? " WHERE" : " AND";
253 filterString += " TIME<='" + JDBC_DATE_FORMAT.format(endDate.withZoneSameInstant(timeZone)) + "'";
255 filterString += (filter.getOrdering() == Ordering.ASCENDING) ? " ORDER BY time ASC" : " ORDER BY time DESC";
256 if (filter.getPageSize() != 0x7fffffff) {
258 // filterString += " LIMIT " + filter.getPageNumber() *
259 // filter.getPageSize() + "," + filter.getPageSize();
260 // SELECT time, value FROM ohscriptfiles_sw_ace_paths_0001 ORDER BY
261 // time DESC OFFSET 1 ROWS FETCH NEXT 0 ROWS ONLY
262 // filterString += " OFFSET " + filter.getPageSize() +" ROWS FETCH
263 // FIRST||NEXT " + filter.getPageNumber() * filter.getPageSize() + "
265 filterString += " OFFSET " + filter.getPageSize() + " ROWS FETCH FIRST "
266 + (filter.getPageNumber() * filter.getPageSize() + 1) + " ROWS ONLY";
269 // http://www.seemoredata.com/en/showthread.php?132-Round-function-in-Apache-Derby
270 // simulated round function in Derby: CAST(value 0.0005 AS DECIMAL(15,3))
271 // simulated round function in Derby: "CAST(value 0.0005 AS DECIMAL(15,"+numberDecimalcount+"))"
273 String queryString = "SELECT time,";
274 if ("NUMBERITEM".equalsIgnoreCase(simpleName) && numberDecimalcount > -1) {
276 queryString += "CAST(value 0.";
277 for (int i = 0; i < numberDecimalcount; i++) {
280 queryString += "5 AS DECIMAL(31," + numberDecimalcount + "))"; // 31 is DECIMAL max precision
281 // https://db.apache.org/derby/docs/10.0/manuals/develop/develop151.html
283 queryString += " value FROM " + table.toUpperCase();
286 if (!filterString.isEmpty()) {
287 queryString += filterString;
289 logger.debug("JDBC::query queryString = {}", queryString);
297 /******************************
298 * public Getters and Setters *
299 ******************************/