2 * Copyright (c) 2010-2023 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;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.knowm.yank.Yank;
21 import org.knowm.yank.exceptions.YankSQLException;
22 import org.openhab.core.items.Item;
23 import org.openhab.core.persistence.FilterCriteria;
24 import org.openhab.core.persistence.FilterCriteria.Ordering;
25 import org.openhab.core.types.State;
26 import org.openhab.persistence.jdbc.internal.dto.Column;
27 import org.openhab.persistence.jdbc.internal.dto.ItemVO;
28 import org.openhab.persistence.jdbc.internal.dto.ItemsVO;
29 import org.openhab.persistence.jdbc.internal.exceptions.JdbcSQLException;
30 import org.openhab.persistence.jdbc.internal.utils.DbMetaData;
31 import org.openhab.persistence.jdbc.internal.utils.StringUtilsExt;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * Extended Database Configuration class. Class represents
37 * the extended database-specific configuration. Overrides and supplements the
38 * default settings from JdbcBaseDAO. Enter only the differences to JdbcBaseDAO here.
40 * @author Helmut Lehmeyer - Initial contribution
43 public class JdbcPostgresqlDAO extends JdbcBaseDAO {
44 private static final String DRIVER_CLASS_NAME = org.postgresql.Driver.class.getName();
45 @SuppressWarnings("unused")
46 private static final String DATA_SOURCE_CLASS_NAME = org.postgresql.ds.PGSimpleDataSource.class.getName();
48 private final Logger logger = LoggerFactory.getLogger(JdbcPostgresqlDAO.class);
53 public JdbcPostgresqlDAO() {
59 private void initSqlQueries() {
60 logger.debug("JDBC::initSqlQueries: '{}'", this.getClass().getSimpleName());
61 // System Information Functions: https://www.postgresql.org/docs/9.2/static/functions-info.html
62 sqlGetDB = "SELECT CURRENT_DATABASE()";
63 sqlIfTableExists = "SELECT * FROM PG_TABLES WHERE TABLENAME='#searchTable#'";
64 sqlCreateItemsTableIfNot = "CREATE TABLE IF NOT EXISTS #itemsManageTable# (itemid SERIAL NOT NULL, #colname# #coltype# NOT NULL, CONSTRAINT #itemsManageTable#_pkey PRIMARY KEY (itemid))";
65 sqlCreateNewEntryInItemsTable = "INSERT INTO items (itemname) SELECT itemname FROM #itemsManageTable# UNION VALUES ('#itemname#') EXCEPT SELECT itemname FROM items";
66 sqlGetItemTables = "SELECT table_name FROM information_schema.tables WHERE table_type='BASE TABLE' AND table_schema=(SELECT table_schema "
67 + "FROM information_schema.tables WHERE table_type='BASE TABLE' AND table_name='#itemsManageTable#') AND NOT table_name='#itemsManageTable#'";
68 // The PostgreSQL equivalent to MySQL columns.column_type is data_type (e.g. "timestamp with time zone") and
69 // udt_name which contains a shorter alias (e.g. "timestamptz"). We alias data_type as "column_type" and
70 // udt_name as "column_type_alias" to be compatible with the 'Column' class used in Yank.queryBeanList
71 sqlGetTableColumnTypes = "SELECT column_name, data_type as column_type, udt_name as column_type_alias, is_nullable FROM information_schema.columns "
72 + "WHERE table_name='#tableName#' AND table_catalog='#jdbcUriDatabaseName#' AND table_schema=(SELECT table_schema FROM information_schema.tables WHERE table_type='BASE TABLE' "
73 + "AND table_name='#itemsManageTable#')";
74 // NOTICE: on PostgreSql >= 9.5, sqlInsertItemValue query template is modified to do an "upsert" (overwrite
75 // existing value). The version check and query change is performed at initAfterFirstDbConnection()
76 sqlInsertItemValue = "INSERT INTO #tableName# (TIME, VALUE) VALUES( #tablePrimaryValue#, CAST( ? as #dbType#) )";
77 sqlAlterTableColumn = "ALTER TABLE #tableName# ALTER COLUMN #columnName# TYPE #columnType#";
81 public void initAfterFirstDbConnection() {
82 logger.debug("JDBC::initAfterFirstDbConnection: Initializing step, after db is connected.");
83 DbMetaData dbMeta = new DbMetaData();
85 // Perform "upsert" (on PostgreSql >= 9.5): Overwrite previous VALUE if same TIME (Primary Key) is provided
86 // This is the default at JdbcBaseDAO and is equivalent to MySQL: ON DUPLICATE KEY UPDATE VALUE
87 // see: https://www.postgresql.org/docs/9.5/sql-insert.html
88 if (dbMeta.isDbVersionGreater(9, 4)) {
89 logger.debug("JDBC::initAfterFirstDbConnection: Values with the same time will be upserted (Pg >= 9.5)");
90 sqlInsertItemValue = "INSERT INTO #tableName# (TIME, VALUE) VALUES( #tablePrimaryValue#, CAST( ? as #dbType#) )"
91 + " ON CONFLICT (TIME) DO UPDATE SET VALUE=EXCLUDED.VALUE";
96 * INFO: http://www.java2s.com/Code/Java/Database-SQL-JDBC/StandardSQLDataTypeswithTheirJavaEquivalents.htm
98 private void initSqlTypes() {
99 // Initialize the type array
100 sqlTypes.put("CALLITEM", "VARCHAR");
101 sqlTypes.put("COLORITEM", "VARCHAR");
102 sqlTypes.put("CONTACTITEM", "VARCHAR");
103 sqlTypes.put("DATETIMEITEM", "TIMESTAMPTZ");
104 sqlTypes.put("DIMMERITEM", "SMALLINT");
105 sqlTypes.put("IMAGEITEM", "VARCHAR");
106 sqlTypes.put("LOCATIONITEM", "VARCHAR");
107 sqlTypes.put("NUMBERITEM", "DOUBLE PRECISION");
108 sqlTypes.put("PLAYERITEM", "VARCHAR");
109 sqlTypes.put("ROLLERSHUTTERITEM", "SMALLINT");
110 sqlTypes.put("STRINGITEM", "VARCHAR");
111 sqlTypes.put("SWITCHITEM", "VARCHAR");
112 sqlTypes.put("tablePrimaryKey", "TIMESTAMPTZ");
113 logger.debug("JDBC::initSqlTypes: Initialized the type array sqlTypes={}", sqlTypes.values());
117 * INFO: https://github.com/brettwooldridge/HikariCP
119 private void initDbProps() {
121 // databaseProps.setProperty("dataSource.cachePrepStmts", "true");
122 // databaseProps.setProperty("dataSource.prepStmtCacheSize", "250");
123 // databaseProps.setProperty("dataSource.prepStmtCacheSqlLimit", "2048");
125 // Properties for HikariCP
126 databaseProps.setProperty("driverClassName", DRIVER_CLASS_NAME);
127 // driverClassName OR BETTER USE dataSourceClassName
128 // databaseProps.setProperty("dataSourceClassName", DATA_SOURCE_CLASS_NAME);
129 // databaseProps.setProperty("maximumPoolSize", "3");
130 // databaseProps.setProperty("minimumIdle", "2");
137 public ItemsVO doCreateItemsTableIfNot(ItemsVO vo) throws JdbcSQLException {
138 String sql = StringUtilsExt.replaceArrayMerge(sqlCreateItemsTableIfNot,
139 new String[] { "#itemsManageTable#", "#colname#", "#coltype#", "#itemsManageTable#" },
140 new String[] { vo.getItemsManageTable(), vo.getColname(), vo.getColtype(), vo.getItemsManageTable() });
141 logger.debug("JDBC::doCreateItemsTableIfNot sql={}", sql);
143 Yank.execute(sql, null);
144 } catch (YankSQLException e) {
145 throw new JdbcSQLException(e);
151 public Long doCreateNewEntryInItemsTable(ItemsVO vo) throws JdbcSQLException {
152 String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable,
153 new String[] { "#itemsManageTable#", "#itemname#" },
154 new String[] { vo.getItemsManageTable(), vo.getItemName() });
155 logger.debug("JDBC::doCreateNewEntryInItemsTable sql={}", sql);
157 return Yank.insert(sql, null);
158 } catch (YankSQLException e) {
159 throw new JdbcSQLException(e);
164 public List<ItemsVO> doGetItemTables(ItemsVO vo) throws JdbcSQLException {
165 String sql = StringUtilsExt.replaceArrayMerge(this.sqlGetItemTables,
166 new String[] { "#itemsManageTable#", "#itemsManageTable#" },
167 new String[] { vo.getItemsManageTable(), vo.getItemsManageTable() });
168 this.logger.debug("JDBC::doGetItemTables sql={}", sql);
170 return Yank.queryBeanList(sql, ItemsVO.class, null);
171 } catch (YankSQLException e) {
172 throw new JdbcSQLException(e);
177 * Override because for PostgreSQL a different query is required with a 3rd argument (itemsManageTable)
180 public List<Column> doGetTableColumns(ItemsVO vo) throws JdbcSQLException {
181 String sql = StringUtilsExt.replaceArrayMerge(sqlGetTableColumnTypes,
182 new String[] { "#jdbcUriDatabaseName#", "#tableName#", "#itemsManageTable#" },
183 new String[] { vo.getJdbcUriDatabaseName(), vo.getTableName(), vo.getItemsManageTable() });
184 logger.debug("JDBC::doGetTableColumns sql={}", sql);
186 return Yank.queryBeanList(sql, Column.class, null);
187 } catch (YankSQLException e) {
188 throw new JdbcSQLException(e);
197 * Override since PostgreSQL does not support setting NOT NULL in the same clause as ALTER COLUMN .. TYPE
200 public void doAlterTableColumn(String tableName, String columnName, String columnType, boolean nullable)
201 throws JdbcSQLException {
202 String sql = StringUtilsExt.replaceArrayMerge(sqlAlterTableColumn,
203 new String[] { "#tableName#", "#columnName#", "#columnType#" },
204 new String[] { tableName, columnName, columnType });
205 logger.info("JDBC::doAlterTableColumn sql={}", sql);
207 Yank.execute(sql, null);
209 String sql2 = StringUtilsExt.replaceArrayMerge(
210 "ALTER TABLE #tableName# ALTER COLUMN #columnName# SET NOT NULL",
211 new String[] { "#tableName#", "#columnName#" }, new String[] { tableName, columnName });
212 logger.info("JDBC::doAlterTableColumn sql={}", sql2);
213 Yank.execute(sql2, null);
215 } catch (YankSQLException e) {
216 throw new JdbcSQLException(e);
221 public void doStoreItemValue(Item item, State itemState, ItemVO vo) throws JdbcSQLException {
222 ItemVO storedVO = storeItemValueProvider(item, itemState, vo);
223 String sql = StringUtilsExt.replaceArrayMerge(sqlInsertItemValue,
224 new String[] { "#tableName#", "#dbType#", "#tablePrimaryValue#" },
225 new String[] { storedVO.getTableName(), storedVO.getDbType(), sqlTypes.get("tablePrimaryValue") });
226 Object[] params = { storedVO.getValue() };
227 logger.debug("JDBC::doStoreItemValue sql={} value='{}'", sql, storedVO.getValue());
229 Yank.execute(sql, params);
230 } catch (YankSQLException e) {
231 throw new JdbcSQLException(e);
236 public void doStoreItemValue(Item item, State itemState, ItemVO vo, ZonedDateTime date) throws JdbcSQLException {
237 ItemVO storedVO = storeItemValueProvider(item, itemState, vo);
238 String sql = StringUtilsExt.replaceArrayMerge(sqlInsertItemValue,
239 new String[] { "#tableName#", "#dbType#", "#tablePrimaryValue#" },
240 new String[] { storedVO.getTableName(), storedVO.getDbType(), "?" });
241 java.sql.Timestamp timestamp = new java.sql.Timestamp(date.toInstant().toEpochMilli());
242 Object[] params = { timestamp, storedVO.getValue() };
243 logger.debug("JDBC::doStoreItemValue sql={} timestamp={} value='{}'", sql, timestamp, storedVO.getValue());
245 Yank.execute(sql, params);
246 } catch (YankSQLException e) {
247 throw new JdbcSQLException(e);
251 /****************************
252 * SQL generation Providers *
253 ****************************/
256 protected String histItemFilterQueryProvider(FilterCriteria filter, int numberDecimalcount, String table,
257 String simpleName, ZoneId timeZone) {
259 "JDBC::getHistItemFilterQueryProvider filter = {}, numberDecimalcount = {}, table = {}, simpleName = {}",
260 filter.toString(), numberDecimalcount, table, simpleName);
262 String filterString = "";
263 if (filter.getBeginDate() != null) {
264 filterString += filterString.isEmpty() ? " WHERE" : " AND";
265 filterString += " TIME>='" + JDBC_DATE_FORMAT.format(filter.getBeginDate().withZoneSameInstant(timeZone))
268 if (filter.getEndDate() != null) {
269 filterString += filterString.isEmpty() ? " WHERE" : " AND";
270 filterString += " TIME<='" + JDBC_DATE_FORMAT.format(filter.getEndDate().withZoneSameInstant(timeZone))
273 filterString += (filter.getOrdering() == Ordering.ASCENDING) ? " ORDER BY time ASC" : " ORDER BY time DESC";
274 if (filter.getPageSize() != 0x7fffffff) {
276 // http://www.jooq.org/doc/3.5/manual/sql-building/sql-statements/select-statement/limit-clause/
277 filterString += " OFFSET " + filter.getPageNumber() * filter.getPageSize() + " LIMIT "
278 + filter.getPageSize();
280 String queryString = "NUMBERITEM".equalsIgnoreCase(simpleName) && numberDecimalcount > -1
281 ? "SELECT time, ROUND(CAST (value AS numeric)," + numberDecimalcount + ") FROM " + table
282 : "SELECT time, value FROM " + table;
283 if (!filterString.isEmpty()) {
284 queryString += filterString;
286 logger.debug("JDBC::query queryString = {}", queryString);
294 /******************************
295 * public Getters and Setters *
296 ******************************/