From d5ba2665831ac919695f15c954cd8600fedb657b Mon Sep 17 00:00:00 2001 From: Harshan Liyanage Date: Mon, 22 May 2017 18:10:58 +0530 Subject: [PATCH] Fixed H2 database issue in App-Mgr. --- .../mgt/core/dao/ApplicationDAO.java | 2 + .../ApplicationManagementDAOFactory.java | 6 +- .../dao/impl/AbstractApplicationDAOImpl.java | 54 +++++++ .../application/H2ApplicationDAOImpl.java | 139 ++++++++++++++++++ ...Impl.java => MySQLApplicationDAOImpl.java} | 16 +- .../dbscripts/cdm/application-mgt/h2.sql | 48 +++--- 6 files changed, 233 insertions(+), 32 deletions(-) create mode 100644 components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/application/H2ApplicationDAOImpl.java rename components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/application/{GenericApplicationDAOImpl.java => MySQLApplicationDAOImpl.java} (91%) diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/ApplicationDAO.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/ApplicationDAO.java index 7584ce203da..ae07f81c586 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/ApplicationDAO.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/ApplicationDAO.java @@ -33,4 +33,6 @@ public interface ApplicationDAO { void deleteApplication (Application application)throws ApplicationManagementDAOException; + int getApplicationCount(Filter filter) throws ApplicationManagementDAOException; + } diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/common/ApplicationManagementDAOFactory.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/common/ApplicationManagementDAOFactory.java index 9ba466e4191..c8eb610ae88 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/common/ApplicationManagementDAOFactory.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/common/ApplicationManagementDAOFactory.java @@ -23,7 +23,8 @@ import org.apache.commons.logging.LogFactory; import org.wso2.carbon.device.application.mgt.common.exception.UnsupportedDatabaseEngineException; import org.wso2.carbon.device.application.mgt.core.config.datasource.DataSourceConfig; import org.wso2.carbon.device.application.mgt.core.dao.ApplicationDAO; -import org.wso2.carbon.device.application.mgt.core.dao.impl.application.GenericApplicationDAOImpl; +import org.wso2.carbon.device.application.mgt.core.dao.impl.application.H2ApplicationDAOImpl; +import org.wso2.carbon.device.application.mgt.core.dao.impl.application.MySQLApplicationDAOImpl; import org.wso2.carbon.device.application.mgt.core.util.ApplicationManagerConstants; import org.wso2.carbon.device.application.mgt.core.util.ConnectionManagerUtil; @@ -55,8 +56,9 @@ public class ApplicationManagementDAOFactory { if (databaseEngine != null) { switch (databaseEngine) { case ApplicationManagerConstants.DataBaseTypes.DB_TYPE_H2: + return new H2ApplicationDAOImpl(); case ApplicationManagerConstants.DataBaseTypes.DB_TYPE_MYSQL: - return new GenericApplicationDAOImpl(); + return new MySQLApplicationDAOImpl(); default: throw new UnsupportedDatabaseEngineException("Unsupported database engine : " + databaseEngine); } diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/AbstractApplicationDAOImpl.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/AbstractApplicationDAOImpl.java index e685e0b0b11..b30a8f0d9bd 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/AbstractApplicationDAOImpl.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/AbstractApplicationDAOImpl.java @@ -21,12 +21,17 @@ package org.wso2.carbon.device.application.mgt.core.dao.impl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.device.application.mgt.common.Application; +import org.wso2.carbon.device.application.mgt.common.Filter; import org.wso2.carbon.device.application.mgt.common.exception.DBConnectionException; import org.wso2.carbon.device.application.mgt.core.dao.ApplicationDAO; import org.wso2.carbon.device.application.mgt.core.dao.common.ApplicationManagementDAOException; +import org.wso2.carbon.device.application.mgt.core.dao.common.ApplicationManagementDAOUtil; import org.wso2.carbon.device.application.mgt.core.util.ConnectionManagerUtil; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; public abstract class AbstractApplicationDAOImpl implements ApplicationDAO { @@ -50,4 +55,53 @@ public abstract class AbstractApplicationDAOImpl implements ApplicationDAO { private Connection getConnection() throws DBConnectionException { return ConnectionManagerUtil.getConnection(); } + + @Override + public int getApplicationCount(Filter filter) throws ApplicationManagementDAOException { + if(log.isDebugEnabled()){ + log.debug("Getting application count from the database"); + log.debug(String.format("Filter: limit=%s, offset=%", filter.getLimit(), filter.getOffset())); + } + + Connection conn; + PreparedStatement stmt = null; + ResultSet rs = null; + String sql = ""; + int count = 0; + + if (filter == null) { + throw new ApplicationManagementDAOException("Filter need to be instantiated"); + } + + try { + conn = this.getConnection(); + sql += "SELECT COUNT(APP.ID) AS APP_COUNT "; + sql += "FROM APPM_APPLICATION AS APP "; + sql += "INNER JOIN APPM_PLATFORM_APPLICATION_MAPPING AS APM ON APP.PLATFORM_APPLICATION_MAPPING_ID = APM.ID "; + sql += "INNER JOIN APPM_PLATFORM AS APL ON APM.PLATFORM_ID = APL.ID "; + sql += "INNER JOIN APPM_APPLICATION_CATEGORY AS CAT ON APP.APPLICATION_CATEGORY_ID = CAT.ID "; + + if (filter.getSearchQuery() != null && !filter.getSearchQuery().isEmpty()) { + sql += "WHERE APP.NAME LIKE ? "; + } + sql += ";"; + + stmt = conn.prepareStatement(sql); + int index = 0; + if (filter.getSearchQuery() != null && !filter.getSearchQuery().isEmpty()) { + stmt.setString(++index, "%" + filter.getSearchQuery() + "%"); + } + rs = stmt.executeQuery(); + if (rs.next()) { + count = rs.getInt("APP_COUNT"); + } + } catch (SQLException e) { + throw new ApplicationManagementDAOException("Error occurred while getting application List", e); + } catch (DBConnectionException e) { + throw new ApplicationManagementDAOException("Error occurred while obtaining the DB connection.", e); + } finally { + ApplicationManagementDAOUtil.cleanupResources(stmt, rs); + } + return count; + } } diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/application/H2ApplicationDAOImpl.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/application/H2ApplicationDAOImpl.java new file mode 100644 index 00000000000..06e40c5fe71 --- /dev/null +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/application/H2ApplicationDAOImpl.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.device.application.mgt.core.dao.impl.application; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.wso2.carbon.device.application.mgt.common.Application; +import org.wso2.carbon.device.application.mgt.common.ApplicationList; +import org.wso2.carbon.device.application.mgt.common.Filter; +import org.wso2.carbon.device.application.mgt.common.Pagination; +import org.wso2.carbon.device.application.mgt.common.exception.DBConnectionException; +import org.wso2.carbon.device.application.mgt.core.dao.common.ApplicationManagementDAOException; +import org.wso2.carbon.device.application.mgt.core.dao.common.ApplicationManagementDAOUtil; +import org.wso2.carbon.device.application.mgt.core.dao.impl.AbstractApplicationDAOImpl; +import org.wso2.carbon.device.application.mgt.core.util.ConnectionManagerUtil; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * This class holds the generic implementation of ApplicationDAO which can be used to support ANSI db syntax. + */ +public class H2ApplicationDAOImpl extends AbstractApplicationDAOImpl { + + private static final Log log = LogFactory.getLog(H2ApplicationDAOImpl.class); + + @Override + public ApplicationList getApplications(Filter filter) throws ApplicationManagementDAOException { + + if(log.isDebugEnabled()){ + log.debug("Getting application data from the database"); + log.debug(String.format("Filter: limit=%s, offset=%", filter.getLimit(), filter.getOffset())); + } + + Connection conn = null; + PreparedStatement stmt = null; + ResultSet rs = null; + String sql = ""; + ApplicationList applicationList = new ApplicationList(); + List applications = new ArrayList<>(); + Pagination pagination = new Pagination(); + + if (filter == null) { + throw new ApplicationManagementDAOException("Filter need to be instantiated"); + } else { + pagination.setLimit(filter.getLimit()); + pagination.setOffset(filter.getOffset()); + } + + try { + + conn = this.getConnection(); + + sql += "SELECT APP.*, APL.NAME AS APL_NAME, APL.IDENTIFIER AS APL_IDENTIFIER," + + " CAT.NAME AS CAT_NAME "; + sql += "FROM APPM_APPLICATION AS APP "; + sql += "INNER JOIN APPM_PLATFORM_APPLICATION_MAPPING AS APM ON APP.PLATFORM_APPLICATION_MAPPING_ID = APM.ID "; + sql += "INNER JOIN APPM_PLATFORM AS APL ON APM.PLATFORM_ID = APL.ID "; + sql += "INNER JOIN APPM_APPLICATION_CATEGORY AS CAT ON APP.APPLICATION_CATEGORY_ID = CAT.ID "; + + if (filter.getSearchQuery() != null && !filter.getSearchQuery().isEmpty()) { + sql += "WHERE APP.NAME LIKE ? "; + } + sql += "LIMIT ?,?;"; + + stmt = conn.prepareStatement(sql); + int index = 0; + if (filter.getSearchQuery() != null && !filter.getSearchQuery().isEmpty()) { + stmt.setString(++index, "%" + filter.getSearchQuery() + "%"); + } + stmt.setInt(++index, filter.getOffset()); + stmt.setInt(++index, filter.getLimit()); + + rs = stmt.executeQuery(); + + int length = 0; + + while (rs.next()) { + + //Getting properties + sql = "SELECT * FROM APPM_APPLICATION_PROPERTY WHERE APPLICATION_ID=?"; + stmt = conn.prepareStatement(sql); + stmt.setInt(1, rs.getInt("ID")); + ResultSet rsProperties = stmt.executeQuery(); + + //Getting tags + sql = "SELECT * FROM APPM_APPLICATION_TAG WHERE APPLICATION_ID=?"; + stmt = conn.prepareStatement(sql); + stmt.setInt(1, rs.getInt("ID")); + ResultSet rsTags = stmt.executeQuery(); + + applications.add(ApplicationManagementDAOUtil.loadApplication(rs, rsProperties, rsTags)); + ApplicationManagementDAOUtil.cleanupResources(null, rsProperties); + ApplicationManagementDAOUtil.cleanupResources(null, rsTags); + length++; + } + + pagination.setSize(length); + pagination.setCount(this.getApplicationCount(filter)); + applicationList.setApplications(applications); + applicationList.setPagination(pagination); + + } catch (SQLException e) { + throw new ApplicationManagementDAOException("Error occurred while getting application List", e); + } catch (JSONException e) { + throw new ApplicationManagementDAOException("Error occurred while parsing JSON", e); + } catch (DBConnectionException e) { + throw new ApplicationManagementDAOException("Error occurred while obtaining the DB connection.", e); + } finally { + ApplicationManagementDAOUtil.cleanupResources(stmt, rs); + } + return applicationList; + } + + private Connection getConnection() throws DBConnectionException { + return ConnectionManagerUtil.getConnection(); + } +} diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/application/GenericApplicationDAOImpl.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/application/MySQLApplicationDAOImpl.java similarity index 91% rename from components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/application/GenericApplicationDAOImpl.java rename to components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/application/MySQLApplicationDAOImpl.java index 51386c713a6..7e6e5358f95 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/application/GenericApplicationDAOImpl.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/dao/impl/application/MySQLApplicationDAOImpl.java @@ -41,9 +41,9 @@ import java.util.List; /** * This class holds the generic implementation of ApplicationDAO which can be used to support ANSI db syntax. */ -public class GenericApplicationDAOImpl extends AbstractApplicationDAOImpl { +public class MySQLApplicationDAOImpl extends AbstractApplicationDAOImpl { - private static final Log log = LogFactory.getLog(GenericApplicationDAOImpl.class); + private static final Log log = LogFactory.getLog(MySQLApplicationDAOImpl.class); @Override public ApplicationList getApplications(Filter filter) throws ApplicationManagementDAOException { @@ -72,25 +72,25 @@ public class GenericApplicationDAOImpl extends AbstractApplicationDAOImpl { conn = this.getConnection(); - sql += "SELECT SQL_CALC_FOUND_ROWS APP.* , APL.NAME AS APL_NAME, APL.IDENTIFIER AS APL_IDENTIFIER," + + sql += "SELECT SQL_CALC_FOUND_ROWS APP.*, APL.NAME AS APL_NAME, APL.IDENTIFIER AS APL_IDENTIFIER," + " CAT.NAME AS CAT_NAME "; sql += "FROM APPM_APPLICATION AS APP "; sql += "INNER JOIN APPM_PLATFORM_APPLICATION_MAPPING AS APM ON APP.PLATFORM_APPLICATION_MAPPING_ID = APM.ID "; sql += "INNER JOIN APPM_PLATFORM AS APL ON APM.PLATFORM_ID = APL.ID "; sql += "INNER JOIN APPM_APPLICATION_CATEGORY AS CAT ON APP.APPLICATION_CATEGORY_ID = CAT.ID "; - if (filter.getSearchQuery() != null || "".equals(filter.getSearchQuery())) { + + if (filter.getSearchQuery() != null && !filter.getSearchQuery().isEmpty()) { sql += "WHERE APP.NAME LIKE ? "; } - sql += "LIMIT ? "; - sql += "OFFSET ?;"; + sql += "LIMIT ?,?;"; stmt = conn.prepareStatement(sql); int index = 0; - if (filter.getSearchQuery() != null || "".equals(filter.getSearchQuery())) { + if (filter.getSearchQuery() != null && !filter.getSearchQuery().isEmpty()) { stmt.setString(++index, "%" + filter.getSearchQuery() + "%"); } - stmt.setInt(++index, filter.getLimit()); stmt.setInt(++index, filter.getOffset()); + stmt.setInt(++index, filter.getLimit()); rs = stmt.executeQuery(); diff --git a/features/application-mgt/org.wso2.carbon.device.application.mgt.server.feature/src/main/resources/dbscripts/cdm/application-mgt/h2.sql b/features/application-mgt/org.wso2.carbon.device.application.mgt.server.feature/src/main/resources/dbscripts/cdm/application-mgt/h2.sql index fde8a0dd7d0..32e670c95bd 100644 --- a/features/application-mgt/org.wso2.carbon.device.application.mgt.server.feature/src/main/resources/dbscripts/cdm/application-mgt/h2.sql +++ b/features/application-mgt/org.wso2.carbon.device.application.mgt.server.feature/src/main/resources/dbscripts/cdm/application-mgt/h2.sql @@ -41,6 +41,20 @@ CREATE TABLE IF NOT EXISTS APPM_PLATFORM_APPLICATION_MAPPING ( ON UPDATE NO ACTION); CREATE INDEX FK_PLATFROM_APPLICATION_MAPPING_PLATFORM ON APPM_PLATFORM_APPLICATION_MAPPING(PLATFORM_ID ASC); + + +-- ----------------------------------------------------- +-- Table `APPM_LIFECYCLE_STATE` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS APPM_LIFECYCLE_STATE ( + ID INT NOT NULL AUTO_INCREMENT, + NAME VARCHAR(100) NOT NULL, + IDENTIFIER VARCHAR(100) NOT NULL, + DESCRIPTION TEXT NULL, + PRIMARY KEY (ID), + UNIQUE INDEX APPM_LIFECYCLE_STATE_IDENTIFIER_UNIQUE (IDENTIFIER ASC)); + + -- ----------------------------------------------------- -- Table APPM_APPLICATION -- ----------------------------------------------------- @@ -57,10 +71,12 @@ CREATE TABLE IF NOT EXISTS APPM_APPLICATION ( CREATED_BY VARCHAR(255) NULL, CREATED_AT DATETIME NOT NULL, MODIFIED_AT DATETIME NULL, - PUBLISHED TINYINT NULL, APPLICATION_CATEGORY_ID INT NOT NULL, PLATFORM_APPLICATION_MAPPING_ID INT NOT NULL, - PRIMARY KEY (ID, APPLICATION_CATEGORY_ID, PLATFORM_APPLICATION_MAPPING_ID), + APPM_LIFECYCLE_STATE_ID INT NOT NULL, + LIECYCLE_STATE_MODIFIED_BY VARCHAR(255) NULL, + LIFECYCLE_STATE_MODIFIED_AT DATETIME NULL, + PRIMARY KEY (ID, APPLICATION_CATEGORY_ID, PLATFORM_APPLICATION_MAPPING_ID, APPM_LIFECYCLE_STATE_ID), UNIQUE INDEX UUID_UNIQUE (UUID ASC), CONSTRAINT FK_APPLICATION_APPLICATION_CATEGORY FOREIGN KEY (APPLICATION_CATEGORY_ID) @@ -71,6 +87,11 @@ CREATE TABLE IF NOT EXISTS APPM_APPLICATION ( FOREIGN KEY (PLATFORM_APPLICATION_MAPPING_ID) REFERENCES APPM_PLATFORM_APPLICATION_MAPPING (ID) ON DELETE NO ACTION + ON UPDATE NO ACTION, + CONSTRAINT fk_APPM_APPLICATION_APPM_LIFECYCLE_STATE1 + FOREIGN KEY (APPM_LIFECYCLE_STATE_ID) + REFERENCES APPM_LIFECYCLE_STATE (ID) + ON DELETE NO ACTION ON UPDATE NO ACTION); CREATE INDEX FK_APPLICATION_APPLICATION_CATEGORY ON APPM_APPLICATION(APPLICATION_CATEGORY_ID ASC); @@ -90,16 +111,6 @@ CREATE TABLE IF NOT EXISTS APPM_APPLICATION_PROPERTY ( ON UPDATE NO ACTION); CREATE INDEX FK_APPLICATION_PROPERTY_APPLICATION ON APPM_APPLICATION_PROPERTY(APPLICATION_ID ASC); --- ----------------------------------------------------- --- Table APPM_LIFECYCLE_STATE --- ----------------------------------------------------- -CREATE TABLE IF NOT EXISTS APPM_LIFECYCLE_STATE ( - ID INT NOT NULL AUTO_INCREMENT, - NAME VARCHAR(100) NOT NULL, - IDENTIFIER VARCHAR(100) NOT NULL, - DESCRIPTION TEXT NULL, - PRIMARY KEY (ID), - UNIQUE INDEX IDENTIFIER_UNIQUE1 (IDENTIFIER ASC)); -- ----------------------------------------------------- -- Table APPM_APPLICATION_RELEASE @@ -113,23 +124,15 @@ CREATE TABLE IF NOT EXISTS APPM_APPLICATION_RELEASE ( RELEASE_DETAILS TEXT NULL, CREATED_AT DATETIME NOT NULL, APPM_APPLICATION_ID INT NOT NULL, - APPM_LIFECYCLE_STATE_ID INT NOT NULL, - LIECYCLE_STATE_MODIFIED_BY VARCHAR(45) NULL, - LIFECYCLE_STATE_MODIFIED_AT DATETIME NULL, - PRIMARY KEY (ID, APPM_APPLICATION_ID, APPM_LIFECYCLE_STATE_ID), + PUBLISHED TINYINT NULL, + PRIMARY KEY (ID, APPM_APPLICATION_ID), CONSTRAINT FK_APPLICATION_VERSION_APPLICATION FOREIGN KEY (APPM_APPLICATION_ID) REFERENCES APPM_APPLICATION (ID) ON DELETE NO ACTION - ON UPDATE NO ACTION, - CONSTRAINT fk_APPM_APPLICATION_RELEASE_APPM_LIFECYCLE_STATE1 - FOREIGN KEY (APPM_LIFECYCLE_STATE_ID) - REFERENCES APPM_LIFECYCLE_STATE (ID) - ON DELETE NO ACTION ON UPDATE NO ACTION); CREATE INDEX FK_APPLICATION_VERSION_APPLICATION ON APPM_APPLICATION_RELEASE(APPM_APPLICATION_ID ASC); -CREATE INDEX FK_APPLICATION_RELEASE_APPM_LIFECYCLE_STATE ON APPM_APPLICATION_RELEASE(APPM_LIFECYCLE_STATE_ID ASC); -- ----------------------------------------------------- -- Table APPM_RELEASE_PROPERTY @@ -201,6 +204,7 @@ CREATE TABLE IF NOT EXISTS APPM_COMMENT ( CREATED_BY VARCHAR(45) NULL, MODIFIED_AT DATETIME NULL, PUBLISHED TINYINT NULL, + APPROVED TINYINT NULL, PRIMARY KEY (ID, APPLICATION_RELEASE_ID), CONSTRAINT FK_APPLICATION_COMMENTS_APPLICATION_RELEASE FOREIGN KEY (APPLICATION_RELEASE_ID)