diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatabasePlatform.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatabasePlatform.java index 6659d228337..27f3455ca55 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatabasePlatform.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/databaseaccess/DatabasePlatform.java @@ -3805,6 +3805,33 @@ public void writeAddColumnClause(Writer writer, AbstractSession session, TableDe field.appendDBString(writer, session, table); } + /** + * INTERNAL: + * May need to override this method if the platform supports ALTER TABLE DROP COLUMN <column> + * and the generated sql doesn't work. + * Write the string that follows ALTER TABLE to create a sql statement for + * the platform in order to drop existing column from an existing table. + */ + public void writeDropColumnClause(Writer writer, AbstractSession session, TableDefinition table, String fieldName) throws IOException { + writer.write("DROP COLUMN "); + writer.write(fieldName); + } + + /** + * INTERNAL: + * May need to override this method if the platform supports TRUNCATE TABLE <table> + * and the generated sql doesn't work. + * Write the string that creates TRUNCATE TABLE sql statement for the platform in order + * to truncate an existing table. + */ + public void writeTruncateTable(Writer writer, AbstractSession session, TableDefinition table) throws IOException { + String tableName = table.getTable() == null + ? table.getName() + : table.getTable().getName(); + writer.write("TRUNCATE TABLE "); + writer.write(tableName); + } + /** * INTERNAL: * Override this method if the platform supports storing JDBC connection user name during diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/localization/i18n/ExceptionLocalizationResource.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/localization/i18n/ExceptionLocalizationResource.java index 28e799a007d..e8f0b196fdb 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/localization/i18n/ExceptionLocalizationResource.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/localization/i18n/ExceptionLocalizationResource.java @@ -265,7 +265,11 @@ public class ExceptionLocalizationResource extends ListResourceBundle { { "json_pgsql_pgobject_conversion", "Database PGobject conversion failed."}, { "json_pgsql_unknown_type", "Unknown JSON type returned from database."}, { "json_ora21c_jsonvalue_to_oraclevalue", "Could not convert JsonValue to OracleJsonValue."}, - { "json_ora21c_resultset_to_jsonvalue", "Could not convert JDBC ResultSet type to JsonValue."} + { "json_ora21c_resultset_to_jsonvalue", "Could not convert JDBC ResultSet type to JsonValue."}, + { "schema_validation_failed", "Schema validation failed"}, + { "schema_validation_missing_table", "The {0} table vas not found in the schema"}, + { "schema_validation_table_surplus_columns", "The {0} table has surplus columns in the schema"}, + { "schema_validation_table_missing_columns", "The {0} table has missing columns in the schema"} }; /** * Return the lookup table. diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/FieldDefinition.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/FieldDefinition.java index c7992fc5c19..15a66aec3ae 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/FieldDefinition.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/FieldDefinition.java @@ -110,20 +110,41 @@ public FieldDefinition(String name, String typeName) { this.name = name; this.typeName = typeName; } - /** * INTERNAL: * Append the database field definition string to the table creation statement. + * * @param writer Target writer where to write field definition string. * @param session Current session context. * @param table Database table being processed. * @throws ValidationException When invalid or inconsistent data were found. */ public void appendDBString(final Writer writer, final AbstractSession session, - final TableDefinition table) throws ValidationException { + final TableDefinition table) { + appendDBString(writer, session, table, null); + } + + /** + * INTERNAL: + * Append the database field definition string to the table creation/modification statement. + * + * @param writer Target writer where to write field definition string. + * @param session Current session context. + * @param table Database table being processed. + * @param alterSeparator Field definition is part of ALTER/MODIFY COUMN statement when not {@code null} + * and {@code alterSeparator} is appended after column name + * @throws ValidationException When invalid or inconsistent data were found. + */ + public void appendDBString(final Writer writer, final AbstractSession session, + final TableDefinition table, String alterSeparator) throws ValidationException { try { - writer.write(name); - writer.write(" "); + writer.write(name); + writer.write(" "); + + if (alterSeparator != null) { + writer.write(alterSeparator); + writer.write(" "); + } if (getTypeDefinition() != null) { //apply user-defined complete type definition writer.write(typeDefinition); diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/SchemaManager.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/SchemaManager.java index a3d1da864c0..e778d6f0e8a 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/SchemaManager.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/SchemaManager.java @@ -39,8 +39,10 @@ import java.net.URL; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.Vector; +import java.util.function.Consumer; /** *

@@ -1126,6 +1128,25 @@ public void replaceDefaultTables(boolean createSequenceTables, boolean createSeq } } + public void truncateDefaultTables(boolean generateFKConstraints) { + boolean shouldLogExceptionStackTrace = getSession().getSessionLog().shouldLogExceptionStackTrace(); + session.getSessionLog().setShouldLogExceptionStackTrace(false); + + try { + TableCreator tableCreator = getDefaultTableCreator(generateFKConstraints); + tableCreator.truncateTables(session, this, generateFKConstraints); + } catch (DatabaseException exception) { + // Ignore error + } finally { + session.getSessionLog().setShouldLogExceptionStackTrace(shouldLogExceptionStackTrace); + } + } + + public boolean validateDefaultTables(Consumer> onFailed, boolean generateFKConstraints) { + TableCreator tableCreator = getDefaultTableCreator(generateFKConstraints); + return tableCreator.validateTables(session, this, onFailed); + } + public void setSession(DatabaseSessionImpl session) { this.session = session; } diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/TableCreator.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/TableCreator.java index f1a308a179b..c6b7ed1f817 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/TableCreator.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/TableCreator.java @@ -17,7 +17,9 @@ // - 389090: JPA 2.1 DDL Generation Support package org.eclipse.persistence.tools.schemaframework; +import jakarta.persistence.PersistenceException; import org.eclipse.persistence.exceptions.DatabaseException; +import org.eclipse.persistence.internal.databaseaccess.FieldTypeDefinition; import org.eclipse.persistence.internal.helper.DatabaseField; import org.eclipse.persistence.internal.sessions.AbstractRecord; import org.eclipse.persistence.internal.sessions.AbstractSession; @@ -28,13 +30,18 @@ import org.eclipse.persistence.sessions.DatabaseSession; import org.eclipse.persistence.sessions.Session; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; /** * Purpose: This class is responsible for creating the tables defined in the project. @@ -351,6 +358,30 @@ public void replaceTables(DatabaseSession session, SchemaManager schemaManager, replaceTablesAndConstraints(schemaManager, session, createSequenceTables, createSequences); } + void truncateTables(DatabaseSession session, SchemaManager schemaManager, boolean generateFKConstraints) { + TableCreator tableCreator = schemaManager.getDefaultTableCreator(generateFKConstraints); + String sequenceTableName = tableCreator.getSequenceTableName(session); + List tables = tableCreator.getTableDefinitions(); + dropConstraints(session, schemaManager, false); + for (TableDefinition table : tables) { + if (!table.getName().equals(sequenceTableName)) { + try { + Writer stmtWriter = new StringWriter(); + session.getPlatform().writeTruncateTable(stmtWriter, ((AbstractSession) session), table); + ((AbstractSession) session) + .priviledgedExecuteNonSelectingCall( + new org.eclipse.persistence.queries.SQLCall(stmtWriter.toString())); + } catch (DatabaseException ex) { + //Ignore database exception. eg. If there is no table to delete, it gives database exception. + throw ex; + } catch (IOException ex) { + throw new PersistenceException(ex); + } + } + } + createConstraints(tables, session, schemaManager, false); + } + protected void replaceTablesAndConstraints(SchemaManager schemaManager, DatabaseSession session, boolean createSequenceTables, boolean createSequences) { buildConstraints(schemaManager, true); boolean ignore = shouldIgnoreDatabaseException(); @@ -442,6 +473,137 @@ protected void extendTablesAndConstraints(SchemaManager schemaManager, DatabaseS } } + /** + * Validate tables in the database. + * Found issues are passed as {@link List} of {@link TableValidationException} to provided consumer + * when validation failed. + * + * @param session Active database session. + * @param schemaManager Database schema manipulation manager. + * @param onFailed {@link Consumer} to accept {@link List} of {@link TableValidationException} + * containing validation failures. Consumer is called only when validation failed. + * @return Value of {@code true} when validation passed or {@code false} otherwise. + */ + public boolean validateTables(final DatabaseSession session, + final SchemaManager schemaManager, + Consumer> onFailed) { + //final String sequenceTableName = getSequenceTableName(session); + List tableDefinitions = getTableDefinitions(); + List exceptions = new ArrayList<>(tableDefinitions.size()); + tableDefinitions.forEach(tableDefinition -> { + String tableName = tableDefinition.getTable() == null + ? tableDefinition.getName() + : tableDefinition.getTable().getName(); + if (schemaManager.checkTableExists(tableDefinition)) { + List columnsInfo = readColumnInfo((AbstractSession) session, tableDefinition); + if (columnsInfo != null && !columnsInfo.isEmpty()) { + final Map columns = parseColumnInfo((AbstractSession) session, + tableDefinition, + columnsInfo); + // Build list of missing columns + CheckDatabaseColumns check = new CheckDatabaseColumns(session, columns.size()); + processColumnns(tableDefinition, + columns, + check::checkExisting, + check::addMissing, + check::surplusColumns); + // Missing columns + if (!check.getMissingColumns().isEmpty()) { + exceptions.add( + new TableValidationException.MissingColumns(tableName, check.getMissingColumns())); + } + // Surplus columns + if (!check.getSurplusFields().isEmpty()) { + exceptions.add( + new TableValidationException.SurplusColumns( + tableName, + check.getSurplusFields().stream().map(DatabaseField::getName).toList())); + } + if (!check.getExistingColumnsDiff().isEmpty()) { + exceptions.add( + new TableValidationException.DifferentColumns(tableName, check.getExistingColumnsDiff())); + } + } + } else { + exceptions.add(new TableValidationException.MissingTable(tableName)); + } + }); + if (exceptions.isEmpty()) { + return true; + } else { + // Pass validation failures to provided consumer + onFailed.accept(exceptions); + return false; + } + } + + private static final class CheckDatabaseColumns { + + final DatabaseSession session; + final List missingColumns; + final List existingColumnsDiff; + Set surplusFields; + + private CheckDatabaseColumns(DatabaseSession session, int size) { + this.session = session; + this.missingColumns = new ArrayList<>(size); + this.existingColumnsDiff = new LinkedList<>(); + this.surplusFields = null; + } + + // Database columns check callback for missing column (existing in TableDefinition but not in database) + private void addMissing(FieldDefinition fieldDefinition, DatabaseField databaseField) { + missingColumns.add(databaseField.getName()); + } + + // Database columns check callback for column existing in both TableDefinition and the database + private void checkExisting(FieldDefinition fieldDefinition, DatabaseField databaseField, AbstractRecord dbRecord) { + FieldTypeDefinition expectedDbType = DatabaseObjectDefinition.getFieldTypeDefinition(session.getPlatform(), + fieldDefinition.getType(), + fieldDefinition.getTypeName()); + String typeName = (String) dbRecord.get("TYPE_NAME"); + if (typeName != null) { + // Type mismatch + if (!typeName.equals(expectedDbType.getName())) { + existingColumnsDiff.add( + new TableValidationException.DifferentColumns.TypeDifference(databaseField.getName(), + expectedDbType.getName(), + typeName)); + } + } + } + + // Database columns check callback for set of surplus fields (existing in database but not in TableDefinition) + private void surplusColumns(Set databaseFields) { + this.surplusFields = databaseFields; + } + + private Set getSurplusFields() { + return surplusFields; + } + + private List getMissingColumns() { + return missingColumns; + } + + private List getExistingColumnsDiff() { + return existingColumnsDiff; + } + + private interface ExistingField { + void accept(FieldDefinition fieldDefinition, DatabaseField databaseField, AbstractRecord dbRecord); + } + + private interface MissingField { + void accept(FieldDefinition fieldDefinition, DatabaseField databaseField); + } + + private interface SurplusFields { + void accept(Set surplusFields); + } + + } + /** * This creates/extends the tables on the database. * @param session Active database session. @@ -475,72 +637,25 @@ public void extendTables(final DatabaseSession session, final SchemaManager sche if (alreadyExists) { //Assume the table exists, so lookup the column info - //While SQL is case insensitive, getColumnInfo is and will not return the table info unless the name is passed in + //While SQL is case-insensitive, getColumnInfo is and will not return the table info unless the name is passed in //as it is stored internally. - String tableName = table.getTable()==null? table.getName(): table.getTable().getName(); - final boolean usesDelimiting = (table.getTable()!=null && table.getTable().shouldUseDelimiters()); - List columnInfo = null; - - columnInfo = abstractSession.getAccessor().getColumnInfo(tableName, null, abstractSession); - - if (!usesDelimiting && (columnInfo == null || columnInfo.isEmpty()) ) { - tableName = tableName.toUpperCase(); - columnInfo = abstractSession.getAccessor().getColumnInfo(tableName, null, abstractSession); - if (( columnInfo == null || columnInfo.isEmpty()) ){ - tableName = tableName.toLowerCase(); - columnInfo = abstractSession.getAccessor().getColumnInfo(tableName, null, abstractSession); - } - } + + List columnInfo = readColumnInfo(abstractSession, table); + if (columnInfo != null && !columnInfo.isEmpty()) { - //Table exists, add individual fields as necessary - - //hash the table's existing columns by name - final Map columns = new HashMap<>(columnInfo.size()); - final DatabaseField columnNameLookupField = new DatabaseField("COLUMN_NAME"); - final DatabaseField schemaLookupField = new DatabaseField("TABLE_SCHEM"); - boolean schemaMatchFound = false; - // Determine the probably schema for the table, this is a heuristic, so should not cause issues if wrong. - String qualifier = table.getQualifier(); - if ((qualifier == null) || (qualifier.length() == 0)) { - qualifier = session.getDatasourcePlatform().getTableQualifier(); - if ((qualifier == null) || (qualifier.length() == 0)) { - qualifier = session.getLogin().getUserName(); - // Oracle DB DS defined in WLS does not contain user name so it's stored in platform. - if ((qualifier == null) || (qualifier.length() == 0)) { - final DatabasePlatform platform = session.getPlatform(); - if (platform.supportsConnectionUserName()) { - qualifier = platform.getConnectionUserName(); - } + // Table exists, parse read columns + final Map columns = parseColumnInfo(abstractSession, table, columnInfo); + // Add missing fields to the database + processMissingColumnns(table, columns, (fieldDef, dbField) -> { + try { + table.addFieldOnDatabase(abstractSession, fieldDef); + } catch (final DatabaseException addFieldEx) { + session.getSessionLog().log(SessionLog.FINEST, SessionLog.DDL, "cannot_add_field_to_table", dbField.getName(), table.getFullName(), addFieldEx.getMessage()); + if (!shouldIgnoreDatabaseException()) { + throw addFieldEx; } } - } - final boolean checkSchema = (qualifier != null) && (qualifier.length() > 0); - for (final AbstractRecord record : columnInfo) { - final String fieldName = (String)record.get(columnNameLookupField); - if (fieldName != null && fieldName.length() > 0) { - final DatabaseField column = new DatabaseField(fieldName); - if (session.getPlatform().shouldForceFieldNamesToUpperCase()) { - column.useUpperCaseForComparisons(true); - } - final String schema = (String)record.get(schemaLookupField); - // Check the schema as well. Ignore columns for other schema if a schema match is found. - if (schemaMatchFound) { - if (qualifier.equalsIgnoreCase(schema)) { - columns.put(column, record); - } - } else { - if (checkSchema) { - if (qualifier.equalsIgnoreCase(schema)) { - schemaMatchFound = true; - // Remove unmatched columns from other schemas. - columns.clear(); - } - } - // If none of the schemas match what is expected, assume what is expected is wrong, and use all columns. - columns.put(column, record); - } - } - } + }); //Go through each field we need to have in the table to see if it already exists for (final FieldDefinition fieldDef : table.getFields()){ @@ -575,4 +690,121 @@ public void extendTables(final DatabaseSession session, final SchemaManager sche session.getDatasourcePlatform().initIdentitySequences(session, DEFAULT_IDENTITY_GENERATOR); } + + // Reads column information from the database. + private List readColumnInfo(AbstractSession session, TableDefinition table) { + String tableName = table.getTable() == null ? table.getName() : table.getTable().getName(); + boolean notUsesDelimiting = table.getTable() == null || !table.getTable().shouldUseDelimiters(); + + List columnInfo = session.getAccessor().getColumnInfo(tableName, null, session); + if (notUsesDelimiting && (columnInfo == null || columnInfo.isEmpty()) ) { + tableName = tableName.toUpperCase(); + columnInfo = session.getAccessor().getColumnInfo(tableName, null, session); + if (( columnInfo == null || columnInfo.isEmpty()) ){ + tableName = tableName.toLowerCase(); + columnInfo = session.getAccessor().getColumnInfo(tableName, null, session); + } + } + return columnInfo; + } + + // Parse column information read from the database. + private static Map parseColumnInfo(AbstractSession session, + TableDefinition table, + List columnInfo) { + // Hash the table's existing columns by name + final Map columns = new HashMap<>(columnInfo.size()); + final DatabaseField columnNameLookupField = new DatabaseField("COLUMN_NAME"); + final DatabaseField schemaLookupField = new DatabaseField("TABLE_SCHEM"); + boolean schemaMatchFound = false; + // Determine the probable schema for the table, this is a heuristic, so should not cause issues if wrong. + String qualifier = table.getQualifier(); + if ((qualifier == null) || (qualifier.length() == 0)) { + qualifier = session.getDatasourcePlatform().getTableQualifier(); + if ((qualifier == null) || (qualifier.length() == 0)) { + qualifier = session.getLogin().getUserName(); + // Oracle DB DS defined in WLS does not contain username, so it's stored in platform. + if ((qualifier == null) || (qualifier.length() == 0)) { + final DatabasePlatform platform = session.getPlatform(); + if (platform.supportsConnectionUserName()) { + qualifier = platform.getConnectionUserName(); + } + } + } + } + final boolean checkSchema = (qualifier != null) && (qualifier.length() > 0); + for (final AbstractRecord record : columnInfo) { + final String fieldName = (String)record.get(columnNameLookupField); + if (fieldName != null && fieldName.length() > 0) { + final DatabaseField column = new DatabaseField(fieldName); + if (session.getPlatform().shouldForceFieldNamesToUpperCase()) { + column.useUpperCaseForComparisons(true); + } + final String schema = (String)record.get(schemaLookupField); + // Check the schema as well. Ignore columns for other schema if a schema match is found. + if (schemaMatchFound) { + if (qualifier.equalsIgnoreCase(schema)) { + columns.put(column, record); + } + } else { + if (checkSchema) { + if (qualifier.equalsIgnoreCase(schema)) { + schemaMatchFound = true; + // Remove unmatched columns from other schemas. + columns.clear(); + } + } + // If none of the schemas match what is expected, assume what is expected is wrong, and use all columns. + columns.put(column, record); + } + } + } + return columns; + } + + // Run provided action for each column missing in the database. + private static void processMissingColumnns(TableDefinition table, + Map columns, + CheckDatabaseColumns.MissingField missingAction) { + processColumnns(table, columns, null, missingAction, null); + } + + // Run provided action for each column missing in the database. + // Optionally provide set of database columns not present in the TableDefinition. + private static void processColumnns(TableDefinition table, + Map columns, + CheckDatabaseColumns.ExistingField existingAction, + CheckDatabaseColumns.MissingField missingAction, + CheckDatabaseColumns.SurplusFields surplusAction) { + // Surplus database fields if consumer was provided + boolean isSurplusAction = surplusAction != null; + Set surplusSet = isSurplusAction ? new HashSet<>(columns.keySet()) : null; + // Process all fields from TableDefinition + for (final FieldDefinition fieldDef : table.getFields()) { + DatabaseField dbField = fieldDef.getDatabaseField(); + if (dbField == null) { + dbField = new DatabaseField(fieldDef.getName()); + } + // Run action for missing column + AbstractRecord dbColumn = columns.get(dbField); + if (dbColumn == null && missingAction != null) { + missingAction.accept(fieldDef, dbField); + // Handle existing column + } else { + // Run action for existing column + if (existingAction != null) { + existingAction.accept(fieldDef, dbField, dbColumn); + } + // Update surplus columns set + if (isSurplusAction) { + surplusSet.remove(dbField); + } + } + } + // Supply set of database columns not present in the TableDefinition when requested + if (isSurplusAction) { + surplusAction.accept(surplusSet); + } + } + } diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/TableDefinition.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/TableDefinition.java index bbac93712d7..87e2a83c796 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/TableDefinition.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/TableDefinition.java @@ -145,6 +145,30 @@ public Writer buildAddFieldWriter(AbstractSession session, FieldDefinition field return writer; } + /** + * INTERNAL: + * Execute the SQL alter table to drop the field from the table. + */ + public void dropFieldOnDatabase(final AbstractSession session, String fieldName) { + session.priviledgedExecuteNonSelectingCall( + new SQLCall(buildDropFieldWriter(session, fieldName, new StringWriter()).toString())); + } + + /** + * INTERNAL: + * Return the alter table statement to drop the field from the table. + */ + public Writer buildDropFieldWriter(AbstractSession session, String fieldName, Writer writer) throws ValidationException { + try { + writer.write("ALTER TABLE " + getFullName() + " "); + session.getPlatform().writeDropColumnClause(writer, session, this, fieldName); + writer.write(" "); + } catch (IOException ioException) { + throw ValidationException.fileError(ioException); + } + return writer; + } + /** * PUBLIC: * Add a foreign key constraint to the table. diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/TableValidationException.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/TableValidationException.java new file mode 100644 index 00000000000..e63b56e2aa0 --- /dev/null +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/TableValidationException.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 1998, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// 11/29/2023: Tomas Kraus +// - New Jakarta Persistence 3.2 Features +package org.eclipse.persistence.tools.schemaframework; + +import java.util.List; + +import org.eclipse.persistence.internal.localization.ExceptionLocalization; + +public abstract class TableValidationException extends Exception { + private final String table; + private final Type type; + + private TableValidationException(String message, String table, Type type) { + super(message); + this.table = table; + this.type = type; + } + + public String getTable() { + return table; + } + + public Type getType() { + return type; + } + + public abstract T as(Class type); + + public static final class MissingTable extends TableValidationException { + + MissingTable(String table) { + super(ExceptionLocalization.buildMessage( + "schema_validation_missing_table", new String[] {table}), + table, Type.MISSING_TABLE); + } + + public T as(Class cls) { + if (cls == MissingTable.class) { + return cls.cast(this); + } + throw new IllegalArgumentException( + String.format("Cannot cast this TableValidationException implementation as %s", cls.getName())); + } + + } + + public static final class MissingColumns extends TableValidationException { + + private final List columns; + + MissingColumns(String table, List columns) { + super(ExceptionLocalization.buildMessage( + "schema_validation_table_missing_columns", new String[] {table}), + table, Type.MISSING_COLUMNS); + this.columns = columns; + } + + public List getColumns() { + return columns; + } + + public T as(Class cls) { + if (cls == MissingColumns.class) { + return cls.cast(this); + } + throw new IllegalArgumentException( + String.format("Cannot cast this TableValidationException implementation as %s", cls.getName())); + } + + } + + public static final class SurplusColumns extends TableValidationException { + + private final List columns; + + SurplusColumns(String table, List columns) { + super(ExceptionLocalization.buildMessage( + "schema_validation_table_surplus_columns", new String[] {table}), + table, Type.SURPLUS_COLUMNS); + this.columns = columns; + } + + public List getColumns() { + return columns; + } + + public T as(Class cls) { + if (cls == SurplusColumns.class) { + return cls.cast(this); + } + throw new IllegalArgumentException( + String.format("Cannot cast this TableValidationException implementation as %s", cls.getName())); + } + + } + + public static final class DifferentColumns extends TableValidationException { + + private final List differences; + + DifferentColumns(String table, List columns) { + super(ExceptionLocalization.buildMessage( + "schema_validation_table_surplus_columns", new String[] {table}), + table, TableValidationException.Type.SURPLUS_COLUMNS); + this.differences = columns; + } + + public List getDifferences() { + return differences; + } + + public T as(Class cls) { + if (cls == SurplusColumns.class) { + return cls.cast(this); + } + throw new IllegalArgumentException( + String.format("Cannot cast this TableValidationException implementation as %s", cls.getName())); + } + + public static abstract class Difference { + + private final String columnName; + private final Type type; + + private Difference(String columnName, Type type) { + this.columnName = columnName; + this.type = type; + } + + public abstract T as(Class type); + + public String getColumnName() { + return columnName; + } + + public Type getType() { + return type; + } + + } + + public static class TypeDifference extends Difference { + + private final String modelValue; + private final String dbValue; + + public TypeDifference(String columnName, String modelValue, String dbValue) { + super(columnName, Type.TYPE_DIFFERENCE); + this.dbValue = dbValue; + this.modelValue = modelValue; + } + + public T as(Class cls) { + if (cls == TypeDifference.class) { + return cls.cast(this); + } + throw new IllegalArgumentException( + String.format("Cannot cast this Difference implementation as %s", cls.getName())); + } + + public String getDbValue() { + return dbValue; + } + + public String getModelValue() { + return modelValue; + } + + } + + public enum Type { + /** Type difference. */ + TYPE_DIFFERENCE; + } + + } + + public enum Type { + /** Missing table in the schema. */ + MISSING_TABLE, + /** Table with missing columns in the schema. */ + MISSING_COLUMNS, + /** Table with surplus columns in the schema. */ + SURPLUS_COLUMNS; + } + + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Persistence32TableCreator.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Persistence32TableCreator.java deleted file mode 100644 index a2753454dcd..00000000000 --- a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/java/org/eclipse/persistence/testing/models/jpa/persistence32/Persistence32TableCreator.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0, - * or the Eclipse Distribution License v. 1.0 which is available at - * http://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause - */ -package org.eclipse.persistence.testing.models.jpa.persistence32; - -import org.eclipse.persistence.testing.framework.TogglingFastTableCreator; -import org.eclipse.persistence.tools.schemaframework.TableDefinition; - -public class Persistence32TableCreator extends TogglingFastTableCreator { - - public Persistence32TableCreator() { - setName("Persistence32Project"); - addTableDefinition(buildTeamTable()); - addTableDefinition(buildTrainerTable()); - addTableDefinition(buildTypeTable()); - addTableDefinition(buildPokemonTable()); - addTableDefinition(buildPokemonTypeTable()); - addTableDefinition(buildSyntaxEntityTable()); - } - - public static TableDefinition buildTeamTable() { - TableDefinition table = new TableDefinition(); - table.setName("PERSISTENCE32_TEAM"); - table.addField(createNumericPk("ID")); - table.addField(createStringColumn("NAME", 64, false)); - return table; - } - - public static TableDefinition buildTrainerTable() { - TableDefinition table = new TableDefinition(); - table.setName("PERSISTENCE32_TRAINER"); - table.addField(createNumericPk("ID")); - table.addField(createStringColumn("NAME", 64, false)); - table.addField(createNumericFk("TEAM_ID", 15,"PERSISTENCE32_TEAM.ID", false)); - return table; - } - - public static TableDefinition buildTypeTable() { - TableDefinition table = new TableDefinition(); - table.setName("PERSISTENCE32_TYPE"); - table.addField(createNumericPk("ID")); - table.addField(createStringColumn("NAME", 64, false)); - return table; - } - - public static TableDefinition buildPokemonTable() { - TableDefinition table = new TableDefinition(); - table.setName("PERSISTENCE32_POKEMON"); - table.addField(createNumericPk("ID")); - table.addField(createNumericFk("TRAINER_ID", 15,"PERSISTENCE32_TRAINER.ID", true)); - table.addField(createStringColumn("NAME", 64, false)); - return table; - } - - public static TableDefinition buildPokemonTypeTable() { - TableDefinition table = new TableDefinition(); - table.setName("PERSISTENCE32_POKEMON_TYPE"); - table.addField(createNumericFk("POKEMON_ID", "PERSISTENCE32_POKEMON.ID")); - table.addField(createNumericFk("TYPE_ID", "PERSISTENCE32_TYPE.ID")); - return table; - } - - public static TableDefinition buildSyntaxEntityTable() { - TableDefinition table = new TableDefinition(); - table.setName("PERSISTENCE32_SYNTAX_ENTITY"); - table.addField(createNumericPk("ID")); - table.addField(createStringColumn("STR_VAL_1", 128, true)); - table.addField(createStringColumn("STR_VAL_2", 128, true)); - table.addField(createNumericColumn("INT_VAL", 15, true)); - table.addField(createTimeColumn("TIME_VAL", 3, true)); - table.addField(createDateColumn("DATE_VAL", 3, true)); - return table; - } - -} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/resources/META-INF/persistence.xml b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/resources/META-INF/persistence.xml index 81008711a8a..a328f4fc450 100644 --- a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/resources/META-INF/persistence.xml +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/main/resources/META-INF/persistence.xml @@ -11,11 +11,12 @@ SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause --> - + xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence + https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd" + version="3.0"> diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/AbstractPokemon.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/AbstractPokemon.java index c3a767ed9f8..34258ec09f8 100644 --- a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/AbstractPokemon.java +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/AbstractPokemon.java @@ -15,19 +15,14 @@ import java.util.Map; import jakarta.persistence.EntityManager; -import junit.framework.TestSuite; -import org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl; -import org.eclipse.persistence.jpa.JpaEntityManagerFactory; -import org.eclipse.persistence.testing.framework.jpa.junit.JUnitTestCase; -import org.eclipse.persistence.testing.models.jpa.persistence32.Persistence32TableCreator; import org.eclipse.persistence.testing.models.jpa.persistence32.Team; import org.eclipse.persistence.testing.models.jpa.persistence32.Trainer; import org.eclipse.persistence.testing.models.jpa.persistence32.Type; /** - * Abstract jUnit test suite with Pokemon model. + * {@link AbstractSuite} with Pokemon model. */ -public abstract class AbstractPokemon extends JUnitTestCase { +public abstract class AbstractPokemon extends AbstractSuite { // Trainer's teams static final Team[] TEAMS = new Team[] { @@ -68,30 +63,17 @@ public abstract class AbstractPokemon extends JUnitTestCase { }; /** - * Build test suite. - * Adds model test setup as first and model test cleanup as last test - * in the returned tests collection. - * - * @param name name of the suite - * @param tests tests to add to the suite - * @return collection of tests to execute + * Creates an instance of {@link AbstractPokemon}. */ - static TestSuite suite(String name, AbstractPokemon... tests) { - TestSuite suite = new TestSuite(); - suite.setName(name); - suite.addTest(new AbstractPokemon("testSetup") {}); - for (AbstractPokemon test : tests) { - suite.addTest(test); - } - suite.addTest(new AbstractPokemon("testCleanup") {}); - return suite; - } - - JpaEntityManagerFactory emf = null; - public AbstractPokemon() { + super(); } + /** + * Creates an instance of {@link AbstractPokemon} with custom test case name. + * + * @param name name of the test case + */ public AbstractPokemon(String name) { super(name); setPuName(getPersistenceUnitName()); @@ -102,33 +84,11 @@ public String getPersistenceUnitName() { return "persistence32"; } - @Override - public void setUp() { - super.setUp(); - emf = getEntityManagerFactory(getPersistenceUnitName()) - .unwrap(EntityManagerFactoryImpl.class); - } - /** - * Return all pokemon types as ID {@link Map}. - * - * @param em {@link EntityManager} instance to execute the query - * @return {@link Map} with pokemon types - */ - Map pokemonTypes(EntityManager em) { - Map types = new HashMap<>(TYPES.length); - em.createNamedQuery("Type.all", Type.class) - .getResultList() - .forEach(type -> types.put(type.getId(), type)); - return types; - } - - /** - * The setup is done as a test, both to record its failure, and to allow - * execution in the server. - */ - public void testSetup() { - new Persistence32TableCreator().replaceTables(JUnitTestCase.getServerSession(getPersistenceUnitName())); + // Initialize data + @Override + protected void suiteSetUp() { + super.suiteSetUp(); emf.runInTransaction(em -> { for (int i = 1; i < TEAMS.length; i++) { em.persist(TEAMS[i]); @@ -144,23 +104,17 @@ public void testSetup() { } /** - * The setup is done as a test, both to record its failure, and to allow - * execution in the server. + * Return all pokemon types as ID {@link Map}. + * + * @param em {@link EntityManager} instance to execute the query + * @return {@link Map} with pokemon types */ - public void testCleanup() { - emf.runInTransaction(em -> { - em.createNamedQuery("Pokemon.deleteAllTypes").executeUpdate(); - em.createNamedQuery("Pokemon.deleteAll").executeUpdate(); - em.createNamedQuery("Type.deleteAll").executeUpdate(); - em.createNamedQuery("Trainer.deleteAll").executeUpdate(); - em.createNamedQuery("Team.deleteAll").executeUpdate(); - }); - } - - @Override - public void clearCache() { - emf.getCache().evictAll(); - super.clearCache(); + Map pokemonTypes(EntityManager em) { + Map types = new HashMap<>(TYPES.length); + em.createNamedQuery("Type.all", Type.class) + .getResultList() + .forEach(type -> types.put(type.getId(), type)); + return types; } } diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/AbstractSchemaManager.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/AbstractSchemaManager.java new file mode 100644 index 00000000000..3fb633b06c1 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/AbstractSchemaManager.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 1998, 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ +package org.eclipse.persistence.testing.tests.jpa.persistence32; + +import java.util.Set; + +import junit.framework.TestSuite; +import org.eclipse.persistence.exceptions.DatabaseException; +import org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl; +import org.eclipse.persistence.jpa.JpaEntityManagerFactory; +import org.eclipse.persistence.testing.framework.ReflectionHelper; +import org.eclipse.persistence.testing.framework.jpa.junit.JUnitTestCase; +import org.eclipse.persistence.tools.schemaframework.TableCreator; + +/** + * Test {@link jakarta.persistence.SchemaManager} implementation in EclipseLink. + * This is an abstract class with all common code for child classes with individual tests. + * All those tests are based on database schema modifications, so they shall not run in parallel. + * Each child class shall contain just a single test. + */ +public abstract class AbstractSchemaManager extends JUnitTestCase { + private org.eclipse.persistence.tools.schemaframework.SchemaManager schemaManager; + JpaEntityManagerFactory emf = null; + + public AbstractSchemaManager() { + } + + public AbstractSchemaManager(String name) { + super(name); + setPuName(getPersistenceUnitName()); + } + + @Override + public String getPersistenceUnitName() { + return "persistence32"; + } + + @Override + public void setUp() { + super.setUp(); + emf = getEntityManagerFactory(getPersistenceUnitName()) + .unwrap(EntityManagerFactoryImpl.class); + schemaManager = new org.eclipse.persistence.tools.schemaframework.SchemaManager(emf.getDatabaseSession()); + dropTables(); + } + + @Override + public void tearDown() { + dropTables(); + } + + /** + * Build test suite. + * Schema manager suites may contain just a single test. + * + * @param name name of the suite + * @param test test to add to the suite + * @return collection of tests to execute + */ + static TestSuite suite(String name, AbstractSchemaManager test) { + TestSuite suite = new TestSuite(); + suite.setName(name); + suite.addTest(test); + return suite; + } + + TableCreator getDefaultTableCreator() { + // getDefaultTableCreator has protected access + try { + return ReflectionHelper.invokeMethod("getDefaultTableCreator", + schemaManager, + new Class[] {boolean.class}, + TableCreator.class, + new Object[] {Boolean.TRUE}); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Invocation of getDefaultTableCreator failed", e); + } + } + + void dropTables() { + try { + schemaManager.dropDefaultTables(); + } catch (DatabaseException de) { + emf.getDatabaseSession().logMessage(de.getLocalizedMessage()); + } + } + + void createTables() { + try { + schemaManager.createDefaultTables(true); + } catch (DatabaseException de) { + emf.getDatabaseSession().logMessage(de.getLocalizedMessage()); + } + } + + void checkMissingTable(Set initialMissingTablesSet, Set missingTablesSet, Set checked, String table) { + if (missingTablesSet.contains(table)) { + missingTablesSet.remove(table); + checked.add(table); + } else { + boolean first = true; + StringBuilder sb = new StringBuilder(); + sb.append('['); + for (String missingTable : initialMissingTablesSet) { + if (first) { + first = false; + } else { + sb.append(','); + } + sb.append(missingTable); + } + sb.append(']'); + if (checked.contains(table)) { + fail(String.format("Duplicate table %s entry was found in expected tables Set %s", table, sb.toString())); + } else { + fail(String.format("Table %s was not found in expected tables Set %s", table, sb.toString())); + } + } + } + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/AbstractSuite.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/AbstractSuite.java new file mode 100644 index 00000000000..7382b11f17f --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/AbstractSuite.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ +package org.eclipse.persistence.testing.tests.jpa.persistence32; + +import jakarta.persistence.Persistence; +import junit.framework.TestSuite; +import org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl; +import org.eclipse.persistence.jpa.JpaEntityManagerFactory; +import org.eclipse.persistence.testing.framework.jpa.junit.JUnitTestCase; +import org.eclipse.persistence.testing.framework.junit.JUnitTestCaseHelper; + +/** + * Abstract {@link JUnitTestCase} suite. + * Adds {@link #suiteSetUp()} and {@link #suiteTearDown()} methods executed before and after + * whole test suite execution. + * Contains suite wide {@link jakarta.persistence.EntityManagerFactory} instance initialized + * by {@link #getPersistenceUnitName()}. + * {@link jakarta.persistence.EntityManagerFactory} and database schema are initialized + * in {@link #suiteSetUp()} method. Database schema is dropped in {@link #suiteTearDown()} method. + * + */ +public abstract class AbstractSuite extends JUnitTestCase { + + // Total number of tests in this suite + private static int TEST_COUNT = 0; + // Test counter (decreasing from TEST_COUNT to 0) to trigger suiteSetUp/suiteTearDown + private static int testCounter; + + // EntityManagerFactory instance shared by the whole suite + static JpaEntityManagerFactory emf = null; + + /** + * Build test suite. + * Adds model test setup as first and model test cleanup as last test + * in the returned tests collection. + * Using this metod is mandatory for suite creation. + * + * @param name name of the suite + * @param tests tests to add to the suite + * @return collection of tests to execute + */ + static TestSuite suite(String name, AbstractSuite... tests) { + TestSuite suite = new TestSuite(); + suite.setName(name); + for (AbstractSuite test : tests) { + suite.addTest(test); + } + testCounter = TEST_COUNT = suite.testCount(); + return suite; + } + + /** + * Creates an instance of {@link AbstractSuite}. + */ + public AbstractSuite() { + super(); + } + + /** + * Creates an instance of {@link AbstractSuite} with custom test case name. + * + * @param name name of the test case + */ + public AbstractSuite(String name) { + super(name); + setPuName(getPersistenceUnitName()); + } + + /** + * Initialize the test suite. + * This method is being executed before the whole test suite. This method initializes the suite wide + * {@link jakarta.persistence.EntityManagerFactory} instance and creates the database schema. + * Child class may overwrite this method to do additional initialization but should also call this method too. + */ + protected void suiteSetUp() { + emf = Persistence.createEntityManagerFactory( + getPersistenceUnitName(), + JUnitTestCaseHelper.getDatabaseProperties(getPersistenceUnitName())) + .unwrap(EntityManagerFactoryImpl.class); + emf.getSchemaManager().create(true); + } + + /** + * Clean up the test suite. + * This method is being executed after the whole test suite. This method drops the database schema + * and closes the suite wide {@link jakarta.persistence.EntityManagerFactory} instance. + * Child class may overwrite this method to do additional cleanup but should also call this method too. + */ + protected void suiteTearDown() { + emf.getSchemaManager().drop(true); + emf.close(); + } + + /** + * This method is called before a test is executed. + * This method implements {@link #suiteSetUp()} call, so it shall be called by child class if overwritten. + */ + @Override + public void setUp() { + super.setUp(); + if (testCounter == TEST_COUNT) { + suiteSetUp(); + } + testCounter--; + } + + /** + * This method is called after a test is executed. + * This method implements {@link #suiteTearDown()} call, so it shall be called by child class if overwritten. + */ + @Override + public void tearDown() { + super.tearDown(); + if (testCounter == 0) { + suiteTearDown(); + } + } + + @Override + public void clearCache() { + emf.getCache().evictAll(); + super.clearCache(); + } + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/CriteriaBuilderTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/CriteriaBuilderTest.java index 6c0e1bf0b16..b08dc900998 100644 --- a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/CriteriaBuilderTest.java +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/CriteriaBuilderTest.java @@ -30,14 +30,12 @@ import jakarta.persistence.criteria.ParameterExpression; import jakarta.persistence.criteria.Root; import junit.framework.Test; -import junit.framework.TestSuite; -import org.eclipse.persistence.internal.jpa.EntityManagerFactoryImpl; -import org.eclipse.persistence.jpa.JpaEntityManagerFactory; -import org.eclipse.persistence.testing.framework.jpa.junit.JUnitTestCase; -import org.eclipse.persistence.testing.models.jpa.persistence32.Persistence32TableCreator; import org.eclipse.persistence.testing.models.jpa.persistence32.SyntaxEntity; -public class CriteriaBuilderTest extends JUnitTestCase { +/** + * Verify jakarta.persistence 3.2 API changes in {@link CriteriaBuilder}. + */ +public class CriteriaBuilderTest extends AbstractSuite { // SyntaxEntity instances, array index is equal to ID // Values must be unique to get just single result and verify it by ID @@ -50,47 +48,43 @@ public class CriteriaBuilderTest extends JUnitTestCase { new SyntaxEntity(5L, null, null, null, null, LocalDate.of(1918, 9, 28), null) }; - private static final int ENTITIES_COUNT = ENTITIES.length - 1; - - private JpaEntityManagerFactory emf = null; - public static Test suite() { - TestSuite suite = new TestSuite(); - suite.setName("CriteriaBuilderTests"); - suite.addTest(new CriteriaBuilderTest("testSetup")); - suite.addTest(new CriteriaBuilderTest("testAndPredicateAsListOf0")); - suite.addTest(new CriteriaBuilderTest("testOrPredicateAsListOf0")); - suite.addTest(new CriteriaBuilderTest("testAndPredicateAsListOf1")); - suite.addTest(new CriteriaBuilderTest("testOrPredicateAsListOf1")); - suite.addTest(new CriteriaBuilderTest("testAndPredicateAsListOf2")); - suite.addTest(new CriteriaBuilderTest("testOrPredicateAsListOf2")); - suite.addTest(new CriteriaBuilderTest("testAndPredicateAsListOfN")); - suite.addTest(new CriteriaBuilderTest("testOrPredicateAsListOfN")); - suite.addTest(new CriteriaBuilderTest("testLeftIntLen")); - suite.addTest(new CriteriaBuilderTest("testLeftExprLen")); - suite.addTest(new CriteriaBuilderTest("testRightIntLen")); - suite.addTest(new CriteriaBuilderTest("testRightExprLen")); - suite.addTest(new CriteriaBuilderTest("testReplaceExprExpr")); - suite.addTest(new CriteriaBuilderTest("testReplaceExprStr")); - suite.addTest(new CriteriaBuilderTest("testReplaceStrExpr")); - suite.addTest(new CriteriaBuilderTest("testReplaceStrStr")); - suite.addTest(new CriteriaBuilderTest("testExtractHourFromTime")); - suite.addTest(new CriteriaBuilderTest("testExtractMinuteFromTime")); - suite.addTest(new CriteriaBuilderTest("testExtractSecondFromTime")); - suite.addTest(new CriteriaBuilderTest("testExtractYearFromDate")); - suite.addTest(new CriteriaBuilderTest("testExtractMonthFromDate")); - suite.addTest(new CriteriaBuilderTest("testExtractDayFromDate")); - suite.addTest(new CriteriaBuilderTest("testExtractQuarterFromDate")); - suite.addTest(new CriteriaBuilderTest("testExtractWeekFromDate")); - suite.addTest(new CriteriaBuilderTest("testExpressionEqualToExpression")); - suite.addTest(new CriteriaBuilderTest("testExpressionEqualToObject")); - suite.addTest(new CriteriaBuilderTest("testExpressionNotEqualToExpression")); - suite.addTest(new CriteriaBuilderTest("testExpressionNotEqualToObject")); - suite.addTest(new CriteriaBuilderTest("testExpressionCast")); - return suite; + return suite( + "CriteriaBuilderTests", + new CriteriaBuilderTest("testAndPredicateAsListOf0"), + new CriteriaBuilderTest("testOrPredicateAsListOf0"), + new CriteriaBuilderTest("testAndPredicateAsListOf1"), + new CriteriaBuilderTest("testOrPredicateAsListOf1"), + new CriteriaBuilderTest("testAndPredicateAsListOf2"), + new CriteriaBuilderTest("testOrPredicateAsListOf2"), + new CriteriaBuilderTest("testAndPredicateAsListOfN"), + new CriteriaBuilderTest("testOrPredicateAsListOfN"), + new CriteriaBuilderTest("testLeftIntLen"), + new CriteriaBuilderTest("testLeftExprLen"), + new CriteriaBuilderTest("testRightIntLen"), + new CriteriaBuilderTest("testRightExprLen"), + new CriteriaBuilderTest("testReplaceExprExpr"), + new CriteriaBuilderTest("testReplaceExprStr"), + new CriteriaBuilderTest("testReplaceStrExpr"), + new CriteriaBuilderTest("testReplaceStrStr"), + new CriteriaBuilderTest("testExtractHourFromTime"), + new CriteriaBuilderTest("testExtractMinuteFromTime"), + new CriteriaBuilderTest("testExtractSecondFromTime"), + new CriteriaBuilderTest("testExtractYearFromDate"), + new CriteriaBuilderTest("testExtractMonthFromDate"), + new CriteriaBuilderTest("testExtractDayFromDate"), + new CriteriaBuilderTest("testExtractQuarterFromDate"), + new CriteriaBuilderTest("testExtractWeekFromDate"), + new CriteriaBuilderTest("testExpressionEqualToExpression"), + new CriteriaBuilderTest("testExpressionEqualToObject"), + new CriteriaBuilderTest("testExpressionNotEqualToExpression"), + new CriteriaBuilderTest("testExpressionNotEqualToObject"), + new CriteriaBuilderTest("testExpressionCast") + ); } public CriteriaBuilderTest() { + super(); } public CriteriaBuilderTest(String name) { @@ -104,18 +98,8 @@ public String getPersistenceUnitName() { } @Override - public void setUp() { - super.setUp(); - emf = getEntityManagerFactory(getPersistenceUnitName()).unwrap(EntityManagerFactoryImpl.class); - } - - /** - * The setup is done as a test, both to record its failure, and to allow - * execution in the server. - */ - public void testSetup() { - new Persistence32TableCreator().replaceTables(JUnitTestCase.getServerSession(getPersistenceUnitName())); - clearCache(); + protected void suiteSetUp() { + super.suiteSetUp(); try (EntityManager em = emf.createEntityManager()) { EntityTransaction et = em.getTransaction(); try { diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/EntityManagerFactoryTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/EntityManagerFactoryTest.java index dd5e7f3925b..a04c11817d7 100644 --- a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/EntityManagerFactoryTest.java +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/EntityManagerFactoryTest.java @@ -37,6 +37,9 @@ import org.eclipse.persistence.testing.models.jpa.persistence32.Trainer_; import org.eclipse.persistence.testing.models.jpa.persistence32.Type; +/** + * Verify jakarta.persistence 3.2 API changes in {@link jakarta.persistence.EntityManagerFactory}. + */ public class EntityManagerFactoryTest extends AbstractPokemon { public static Test suite() { diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/QueryTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/QueryTest.java index b4924dd83be..639fffa6963 100644 --- a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/QueryTest.java +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/QueryTest.java @@ -20,6 +20,9 @@ import org.eclipse.persistence.testing.models.jpa.persistence32.Pokemon; import org.junit.Assert; +/** + * Verify jakarta.persistence 3.2 API changes in queries. + */ public class QueryTest extends AbstractPokemon { // Pokemons. Array index is ID value. @@ -35,7 +38,6 @@ public class QueryTest extends AbstractPokemon { public static Test suite() { return suite( "QueryTest", - new QueryTest("testSetup"), new QueryTest("testGetSingleResultWithEmptyResult"), new QueryTest("testGetSingleResultWithSingleResult"), new QueryTest("testGetSingleResultWithMultipleResults"), @@ -55,11 +57,10 @@ public QueryTest(String name) { setPuName(getPersistenceUnitName()); } - /** - * The setup is done as a test, both to record its failure, and to allow - * execution in the server. - */ - public void testSetup() { + // Initialize data + @Override + protected void suiteSetUp() { + super.suiteSetUp(); emf.runInTransaction(em -> { for (int i = 1; i < POKEMONS.length; i++) { em.persist(POKEMONS[i]); diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerCreateTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerCreateTest.java new file mode 100644 index 00000000000..42ff3570232 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerCreateTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ +package org.eclipse.persistence.testing.tests.jpa.persistence32; + +import jakarta.persistence.SchemaManager; +import junit.framework.Test; + +import static org.eclipse.persistence.testing.tests.jpa.persistence32.AbstractPokemon.TEAMS; +import static org.eclipse.persistence.testing.tests.jpa.persistence32.AbstractPokemon.TRAINERS; +import static org.eclipse.persistence.testing.tests.jpa.persistence32.AbstractPokemon.TYPES; + +/** + * Verify jakarta.persistence 3.2 API changes in {@link SchemaManager}. + * Test {@link SchemaManager#create(boolean)} method on database with no schema. + */ +public class SchemaManagerCreateTest extends AbstractSchemaManager { + + public static Test suite() { + return suite( + "SchemaManagerCreateTest", + new SchemaManagerCreateTest("testCreate") + ); + } + public SchemaManagerCreateTest() { + } + + public SchemaManagerCreateTest(String name) { + super(name); + } + + // Test SchemaManager create method + public void testCreate() { + // Tables are always dropped in setUp() method + // Create the schema + SchemaManager schemaManager = emf.getSchemaManager(); + schemaManager.create(true); + // Try to store data into the schema + emf.runInTransaction(em -> { + for (int i = 1; i < TEAMS.length; i++) { + em.persist(TEAMS[i]); + } + for (int i = 1; i < TRAINERS.length; i++) { + em.persist(TRAINERS[i]); + } + for (int i = 1; i < TYPES.length; i++) { + em.persist(TYPES[i]); + } + }); + } + + + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerDropTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerDropTest.java new file mode 100644 index 00000000000..d594e3a0dc7 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerDropTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ +package org.eclipse.persistence.testing.tests.jpa.persistence32; + +import jakarta.persistence.PersistenceException; +import jakarta.persistence.SchemaManager; +import junit.framework.Test; +import org.eclipse.persistence.logging.SessionLog; + +import static org.eclipse.persistence.testing.tests.jpa.persistence32.AbstractPokemon.TEAMS; +import static org.eclipse.persistence.testing.tests.jpa.persistence32.AbstractPokemon.TRAINERS; +import static org.eclipse.persistence.testing.tests.jpa.persistence32.AbstractPokemon.TYPES; + +/** + * Verify jakarta.persistence 3.2 API changes in {@link SchemaManager}. + * Test {@link SchemaManager#drop(boolean)} method on database with already existing schema. + */ +public class SchemaManagerDropTest extends AbstractSchemaManager { + + public static Test suite() { + return suite( + "SchemaManagerDropTest", + new SchemaManagerDropTest("testDrop") + ); + } + public SchemaManagerDropTest() { + } + + public SchemaManagerDropTest(String name) { + super(name); + } + + // Test SchemaManager drop method + public void testDrop() { + // Make sure that tables exist before being dropped + createTables(); + // ...and persist call works + emf.runInTransaction(em -> { + for (int i = 1; i < TEAMS.length; i++) { + em.persist(TEAMS[i]); + } + for (int i = 1; i < TRAINERS.length; i++) { + em.persist(TRAINERS[i]); + } + for (int i = 1; i < TYPES.length; i++) { + em.persist(TYPES[i]); + } + }); + // Drop the schema + SchemaManager schemaManager = emf.getSchemaManager(); + schemaManager.drop(true); + // Turn off logging to suppress expected SQL errors warnings + int logLevel = emf.getDatabaseSession().getSessionLog().getLevel(); + emf.getDatabaseSession().getSessionLog().setLevel(SessionLog.OFF); + // Verify that any attempt to store data throws an exception because of missing tables + // - Team entity + try { + emf.runInTransaction(em -> { + for (int i = 1; i < TEAMS.length; i++) { + em.persist(TEAMS[i]); + } + }); + fail("Calling persist on entity after database schema was deleted shall throw an exception."); + } catch (PersistenceException pe) { + assertTrue( + "Unexpected exception message: " + pe.getLocalizedMessage(), + pe.getLocalizedMessage().contains("does not exist")); + } + // - Trainer entity + try { + emf.runInTransaction(em -> { + for (int i = 1; i < TRAINERS.length; i++) { + em.persist(TRAINERS[i]); + } + }); + fail("Calling persist on entity after database schema was deleted shall throw an exception."); + } catch (PersistenceException pe) { + assertTrue( + "Unexpected exception message: " + pe.getLocalizedMessage(), + pe.getLocalizedMessage().contains("does not exist")); + } + // - Type entity + try { + emf.runInTransaction(em -> { + for (int i = 1; i < TYPES.length; i++) { + em.persist(TYPES[i]); + } + }); + fail("Calling persist on entity after database schema was deleted shall throw an exception."); + } catch (PersistenceException pe) { + assertTrue( + "Unexpected exception message: " + pe.getLocalizedMessage(), + pe.getLocalizedMessage().contains("does not exist")); + } + emf.getDatabaseSession().getSessionLog().setLevel(logLevel); + } + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerTruncateOnExistingTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerTruncateOnExistingTest.java new file mode 100644 index 00000000000..4e45f696c07 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerTruncateOnExistingTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ +package org.eclipse.persistence.testing.tests.jpa.persistence32; + +import jakarta.persistence.SchemaManager; +import junit.framework.Test; + +import static org.eclipse.persistence.testing.tests.jpa.persistence32.AbstractPokemon.TEAMS; +import static org.eclipse.persistence.testing.tests.jpa.persistence32.AbstractPokemon.TRAINERS; +import static org.eclipse.persistence.testing.tests.jpa.persistence32.AbstractPokemon.TYPES; + +/** + * Verify jakarta.persistence 3.2 API changes in {@link SchemaManager}. + * Test {@link SchemaManager#truncate()} method on database with already existing schema and data. + */ +public class SchemaManagerTruncateOnExistingTest extends AbstractSchemaManager { + + public static Test suite() { + return suite( + "SchemaManagerTruncateOnExistingTest", + new SchemaManagerTruncateOnExistingTest("testTruncateOnExistingSchema") + ); + } + public SchemaManagerTruncateOnExistingTest() { + } + + public SchemaManagerTruncateOnExistingTest(String name) { + super(name); + } + + // Test SchemaManager truncate method + public void testTruncateOnExistingSchema() { + // Tables are always dropped in setUp() method + // Make sure that tables exist and contain data + createTables(); + emf.runInTransaction(em -> { + for (int i = 1; i < TEAMS.length; i++) { + em.persist(TEAMS[i]); + } + for (int i = 1; i < TRAINERS.length; i++) { + em.persist(TRAINERS[i]); + } + for (int i = 1; i < TYPES.length; i++) { + em.persist(TYPES[i]); + } + }); + // Truncate the schema + SchemaManager schemaManager = emf.getSchemaManager(); + schemaManager.truncate(); + // Verify that tables still exist but are empty + // - Team count shall be 0 + int teamCount = emf.callInTransaction( + em -> em.createQuery("SELECT count(t) FROM Team t", Integer.class).getFirstResult()); + assertEquals(teamCount, 0); + // - Trainer count shall be 0 + int trainerCount = emf.callInTransaction( + em -> em.createQuery("SELECT count(t) FROM Trainer t", Integer.class).getFirstResult()); + assertEquals(trainerCount, 0); + // - Type count shall be 0 + int typeCount = emf.callInTransaction( + em -> em.createQuery("SELECT count(t) FROM Type t", Integer.class).getFirstResult()); + assertEquals(typeCount, 0); + } + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnMissingColumnTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnMissingColumnTest.java new file mode 100644 index 00000000000..f6fc6a3a9b5 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnMissingColumnTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ +package org.eclipse.persistence.testing.tests.jpa.persistence32; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.persistence.SchemaManager; +import jakarta.persistence.SchemaValidationException; +import junit.framework.Test; +import org.eclipse.persistence.tools.schemaframework.TableCreator; +import org.eclipse.persistence.tools.schemaframework.TableDefinition; +import org.eclipse.persistence.tools.schemaframework.TableValidationException; + +/** + * Verify jakarta.persistence 3.2 API changes in {@link SchemaManager}. + * Test {@link jakarta.persistence.SchemaManager#validate()} method on database with existing but modified schema. + */ +public class SchemaManagerValidateOnMissingColumnTest extends AbstractSchemaManager { + + public static Test suite() { + return suite( + "SchemaManagerValidateOnMissingColumnTest", + new SchemaManagerValidateOnMissingColumnTest("testValidateOnMissingColumn") + ); + } + public SchemaManagerValidateOnMissingColumnTest() { + } + + public SchemaManagerValidateOnMissingColumnTest(String name) { + super(name); + } + + // Test SchemaManager validate method on existing valid schema + public void testValidateOnMissingColumn() { + // Make sure that tables exist + createTables(); + // Modify current schema + TableCreator tableCreator = getDefaultTableCreator(); + Map tableDefinitions = new HashMap<>(tableCreator.getTableDefinitions().size()); + for (TableDefinition tableDefinition : tableCreator.getTableDefinitions()) { + String tableName = tableDefinition.getTable() == null + ? tableDefinition.getName() + : tableDefinition.getTable().getName(); + tableDefinitions.put(tableName, tableDefinition); + } + // Remove "NAME" field from "PERSISTENCE32_TEAM" + TableDefinition team = tableDefinitions.get("PERSISTENCE32_TEAM"); + team.dropFieldOnDatabase(emf.getDatabaseSession(), "NAME"); + // Do the validation + SchemaManager schemaManager = emf.getSchemaManager(); + try { + // Test validation + schemaManager.validate(); + } catch (SchemaValidationException sve) { + // Validation is expected to fail and return all missing columns + Exception[] exceptions = sve.getFailures(); + Map> missingColumns = Map.of("PERSISTENCE32_TEAM", Set.of("NAME")); + for (TableValidationException exception : (TableValidationException[]) exceptions) { + if (!(exception instanceof TableValidationException.MissingColumns)) { + fail("Exception is not an instance of TableValidationException.MissingColumns"); + } + if (exception.getType() == TableValidationException.Type.MISSING_COLUMNS) { + assertEquals("PERSISTENCE32_TEAM", exception.getTable()); + + } else { + fail("Exception type is not MISSING_COLUMNS"); + } + checkMissingColumns(missingColumns, + exception.getTable(), + exception.as(TableValidationException.MissingColumns.class) + .getColumns() + .stream() + .map(String::toUpperCase) + .toList()); + } + } + } + + private void checkMissingColumns(Map> missingColumns, String table, List columns) { + Set columnsToCheck = new HashSet<>(missingColumns.get(table)); + Set checked = new HashSet<>(columnsToCheck.size()); + if (columnsToCheck.isEmpty()) { + fail(String.format("Table %s is not expected to have missing columns", table)); + } + for (String column : columns) { + if (columnsToCheck.contains(column)) { + columnsToCheck.remove(column); + checked.add(column); + } else { + if (checked.contains(column)) { + fail(String.format("Duplicate missing %s column entry for table %s", column, table)); + } else { + fail(String.format("Missing column %s was not reported for table %s", column, table)); + } + } + } + } + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnMissingSchemaTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnMissingSchemaTest.java new file mode 100644 index 00000000000..4731783649f --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnMissingSchemaTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ +package org.eclipse.persistence.testing.tests.jpa.persistence32; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import jakarta.persistence.SchemaManager; +import jakarta.persistence.SchemaValidationException; +import junit.framework.Test; +import org.eclipse.persistence.tools.schemaframework.TableValidationException; + +/** + * Verify jakarta.persistence 3.2 API changes in {@link SchemaManager}. + * Test {@link SchemaManager#validate()} method on database with missing schema. + */ +public class SchemaManagerValidateOnMissingSchemaTest extends AbstractSchemaManager { + + public static Test suite() { + return suite( + "SchemaManagerValidateOnMissingSchemaTest", + new SchemaManagerValidateOnMissingSchemaTest("testValidateOnMissingSchema") + ); + } + public SchemaManagerValidateOnMissingSchemaTest() { + } + + public SchemaManagerValidateOnMissingSchemaTest(String name) { + super(name); + } + + // Test SchemaManager validate method on missing schema + public void testValidateOnMissingSchema() { + // Tables are always dropped in setUp() method + SchemaManager schemaManager = emf.getSchemaManager(); + try { + // Test validation + schemaManager.validate(); + fail("Schema validation shall throw an exception on missing schema"); + } catch (SchemaValidationException sve) { + // Validation is expected to fail and return all tables as missing + Exception[] exceptions = sve.getFailures(); + String[] missingTables = new String[] { + "PERSISTENCE32_TEAM", + "PERSISTENCE32_TRAINER", + "PERSISTENCE32_TYPE", + "PERSISTENCE32_POKEMON", + "PERSISTENCE32_POKEMON_TYPE", + "PERSISTENCE32_SYNTAX_ENTITY", + "PERSISTENCE32_SE_COLTABLE" + }; + Set missingTablesSet = new HashSet<>(Arrays.asList(missingTables)); + Set initialMissingTablesSet = Set.copyOf(missingTablesSet); + Set checked = new HashSet<>(initialMissingTablesSet.size()); + for (TableValidationException exception : (TableValidationException[]) exceptions) { + if (!(exception instanceof TableValidationException.MissingTable)) { + fail("Exception is not an instance of TableValidationException.MissingTable"); + } + if (exception.getType() != TableValidationException.Type.MISSING_TABLE) { + fail("Exception type is not MISSING_TABLE"); + } + checkMissingTable(initialMissingTablesSet, missingTablesSet, checked, exception.getTable()); + } + } + } + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnModifiedColumnTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnModifiedColumnTest.java new file mode 100644 index 00000000000..c93e756da4c --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnModifiedColumnTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ +package org.eclipse.persistence.testing.tests.jpa.persistence32; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.persistence.SchemaManager; +import jakarta.persistence.SchemaValidationException; +import junit.framework.Test; +import org.eclipse.persistence.tools.schemaframework.FieldDefinition; +import org.eclipse.persistence.tools.schemaframework.TableCreator; +import org.eclipse.persistence.tools.schemaframework.TableDefinition; + +/** + * Verify jakarta.persistence 3.2 API changes in {@link SchemaManager}. + * Test {@link jakarta.persistence.SchemaManager#validate()} method on database with existing but modified schema. + */ +public class SchemaManagerValidateOnModifiedColumnTest extends AbstractSchemaManager { + + public static Test suite() { + return suite( + "SchemaManagerValidateOnModifiedColumnTest", + new SchemaManagerValidateOnModifiedColumnTest("testValidateOnModifiedSchema") + ); + } + public SchemaManagerValidateOnModifiedColumnTest() { + } + + public SchemaManagerValidateOnModifiedColumnTest(String name) { + super(name); + } + + // Test SchemaManager validate method on existing valid schema + public void testValidateOnModifiedSchema() { + // Make sure that tables exist + createTables(); + // Modify current schema + TableCreator tableCreator = getDefaultTableCreator(); + Map tableDefinitions = new HashMap<>(tableCreator.getTableDefinitions().size()); + for (TableDefinition tableDefinition : tableCreator.getTableDefinitions()) { + String tableName = tableDefinition.getTable() == null + ? tableDefinition.getName() + : tableDefinition.getTable().getName(); + tableDefinitions.put(tableName, tableDefinition); + } + // Modify "NAME" field in "PERSISTENCE32_TRAINER" + TableDefinition trainer = tableDefinitions.get("PERSISTENCE32_TRAINER"); + FieldDefinition nameField = trainer.getField("NAME"); + nameField.setSize(nameField.getSize()+5); + nameField.setShouldAllowNull(true); + trainer.dropFieldOnDatabase(emf.getDatabaseSession(), "NAME"); + FieldDefinition newNameField = new FieldDefinition(); + newNameField.setName("NAME"); + newNameField.setTypeName("NUMERIC"); + newNameField.setSize(15); + newNameField.setShouldAllowNull(true); + newNameField.setIsPrimaryKey(false); + newNameField.setUnique(false); + newNameField.setIsIdentity(false); + trainer.addFieldOnDatabase(emf.getDatabaseSession(), newNameField); + // Do the validation + SchemaManager schemaManager = emf.getSchemaManager(); + try { + // Test validation + schemaManager.validate(); + } catch (SchemaValidationException sve) { + //fail(sve.getLocalizedMessage()); + } + } + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnSurplusColumnTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnSurplusColumnTest.java new file mode 100644 index 00000000000..f26365fdda1 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnSurplusColumnTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ +package org.eclipse.persistence.testing.tests.jpa.persistence32; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.persistence.SchemaManager; +import jakarta.persistence.SchemaValidationException; +import junit.framework.Test; +import org.eclipse.persistence.tools.schemaframework.FieldDefinition; +import org.eclipse.persistence.tools.schemaframework.TableCreator; +import org.eclipse.persistence.tools.schemaframework.TableDefinition; +import org.eclipse.persistence.tools.schemaframework.TableValidationException; + +/** + * Verify jakarta.persistence 3.2 API changes in {@link SchemaManager}. + * Test {@link jakarta.persistence.SchemaManager#validate()} method on database with existing but modified schema. + */ +public class SchemaManagerValidateOnSurplusColumnTest extends AbstractSchemaManager { + + public static Test suite() { + return suite( + "SchemaManagerValidateOnSurplusColumnTest", + new SchemaManagerValidateOnSurplusColumnTest("testValidateOnModifiedSchema") + ); + } + public SchemaManagerValidateOnSurplusColumnTest() { + } + + public SchemaManagerValidateOnSurplusColumnTest(String name) { + super(name); + } + + // Test SchemaManager validate method on existing valid schema + public void testValidateOnModifiedSchema() { + // Make sure that tables exist + createTables(); + // Modify current schema + TableCreator tableCreator = getDefaultTableCreator(); + Map tableDefinitions = new HashMap<>(tableCreator.getTableDefinitions().size()); + for (TableDefinition tableDefinition : tableCreator.getTableDefinitions()) { + String tableName = tableDefinition.getTable() == null + ? tableDefinition.getName() + : tableDefinition.getTable().getName(); + tableDefinitions.put(tableName, tableDefinition); + } + // Extend PERSISTENCE32_TRAINER with age field + TableDefinition trainer = tableDefinitions.get("PERSISTENCE32_TRAINER"); + FieldDefinition ageField = new FieldDefinition(); + ageField.setName("age"); + ageField.setTypeName("NUMERIC"); + ageField.setSize(15); + ageField.setShouldAllowNull(true); + ageField.setIsPrimaryKey(false); + ageField.setUnique(false); + ageField.setIsIdentity(false); + trainer.addFieldOnDatabase(emf.getDatabaseSession(), ageField); + // Do the validation + SchemaManager schemaManager = emf.getSchemaManager(); + try { + // Test validation + schemaManager.validate(); + } catch (SchemaValidationException sve) { + // Validation is expected to fail and return all missing columns + Exception[] exceptions = sve.getFailures(); + Map> surplusColumns = Map.of("PERSISTENCE32_TRAINER", Set.of("AGE")); + for (TableValidationException exception : (TableValidationException[]) exceptions) { + if (!(exception instanceof TableValidationException.SurplusColumns)) { + fail("Exception is not an instance of TableValidationException.SurplusColumns"); + } + if (exception.getType() == TableValidationException.Type.SURPLUS_COLUMNS) { + assertEquals("PERSISTENCE32_TRAINER", exception.getTable()); + } else { + fail("Exception type is not SURPLUS_COLUMNS"); + } + checkSurplusColumns(surplusColumns, + exception.getTable(), + exception.as(TableValidationException.SurplusColumns.class) + .getColumns() + .stream() + .map(String::toUpperCase) + .toList()); + } + } + } + + private void checkSurplusColumns(Map> surplusColumns, String table, List columns) { + Set columnsToCheck = new HashSet<>(surplusColumns.get(table)); + Set checked = new HashSet<>(columnsToCheck.size()); + if (columnsToCheck.isEmpty()) { + fail(String.format("Table %s is not expected to have missing columns", table)); + } + for (String column : columns) { + if (columnsToCheck.contains(column)) { + columnsToCheck.remove(column); + checked.add(column); + } else { + if (checked.contains(column)) { + fail(String.format("Duplicate missing %s column entry for table %s", column, table)); + } else { + fail(String.format("Missing column %s was not reported for table %s", column, table)); + } + } + } + } + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnValidSchemaTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnValidSchemaTest.java new file mode 100644 index 00000000000..f5573e7396b --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnValidSchemaTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ +package org.eclipse.persistence.testing.tests.jpa.persistence32; + +import jakarta.persistence.SchemaManager; +import jakarta.persistence.SchemaValidationException; +import junit.framework.Test; + +/** + * Verify jakarta.persistence 3.2 API changes in {@link SchemaManager}. + * Test {@link SchemaManager#validate()} method on database with existing and valid schema. + */ +public class SchemaManagerValidateOnValidSchemaTest extends AbstractSchemaManager { + + public static Test suite() { + return suite( + "SchemaManagerValidateOnValidSchemaTest", + new SchemaManagerValidateOnValidSchemaTest("testValidateOnValidSchema") + ); + } + public SchemaManagerValidateOnValidSchemaTest() { + } + + public SchemaManagerValidateOnValidSchemaTest(String name) { + super(name); + } + + // Test SchemaManager validate method on existing valid schema + public void testValidateOnValidSchema() { + // Make sure that tables exist + createTables(); + SchemaManager schemaManager = emf.getSchemaManager(); + try { + // Test validation + schemaManager.validate(); + } catch (SchemaValidationException sve) { + fail(sve.getLocalizedMessage()); + } + } + +} diff --git a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/UnionCriteriaQueryTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/UnionCriteriaQueryTest.java index 96bd487c21c..6739fb2bd35 100644 --- a/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/UnionCriteriaQueryTest.java +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/UnionCriteriaQueryTest.java @@ -30,6 +30,9 @@ import org.eclipse.persistence.testing.models.jpa.persistence32.Pokemon; import org.eclipse.persistence.testing.models.jpa.persistence32.Trainer; +/** + * Verify jakarta.persistence 3.2 API changes in {@link CriteriaBuilder} for union expressions. + */ public class UnionCriteriaQueryTest extends AbstractPokemon { // Pokemons. Array index is ID value. @@ -50,7 +53,6 @@ public class UnionCriteriaQueryTest extends AbstractPokemon { public static Test suite() { return suite( "QueryTest", - new UnionCriteriaQueryTest("testSetup"), new UnionCriteriaQueryTest("testUnionWithNoSelection"), new UnionCriteriaQueryTest("testUnionAllWithNoSelection"), new UnionCriteriaQueryTest("testIntersectWithNoSelection"), @@ -72,11 +74,10 @@ public UnionCriteriaQueryTest(String name) { setPuName(getPersistenceUnitName()); } - /** - * The setup is done as a test, both to record its failure, and to allow - * execution in the server. - */ - public void testSetup() { + // Initialize data + @Override + protected void suiteSetUp() { + super.suiteSetUp(); emf.runInTransaction(em -> { for (int i = 1; i < POKEMONS.length; i++) { em.persist(POKEMONS[i]); @@ -84,22 +85,6 @@ public void testSetup() { }); } - private static void verifyValuesOnce(Set expected, List queryResult) { - Set check = new HashSet<>(expected); - for (Pokemon pokemon : queryResult) { - assertTrue(String.format("Pokemon %d:%s was not found in Set %s", pokemon.getId(), pokemon.getName(), expected), - check.contains(pokemon)); - check.remove(pokemon); - } - } - - private static void verifyValuesMultiple(Set expected, List queryResult) { - for (Pokemon pokemon : queryResult) { - assertTrue(String.format("Pokemon %d:%s was not found in Set %s", pokemon.getId(), pokemon.getName(), expected), - expected.contains(pokemon)); - } - } - // Test UNION: Shall return distinct values from both queries: 1x Pokemon[1..3] // Pokemon[1] matches WHERE clause in both selects public void testUnionWithNoSelection() { @@ -479,4 +464,20 @@ public void testUnionWithMultiselectEntityInSelection() { } } + private static void verifyValuesOnce(Set expected, List queryResult) { + Set check = new HashSet<>(expected); + for (Pokemon pokemon : queryResult) { + assertTrue(String.format("Pokemon %d:%s was not found in Set %s", pokemon.getId(), pokemon.getName(), expected), + check.contains(pokemon)); + check.remove(pokemon); + } + } + + private static void verifyValuesMultiple(Set expected, List queryResult) { + for (Pokemon pokemon : queryResult) { + assertTrue(String.format("Pokemon %d:%s was not found in Set %s", pokemon.getId(), pokemon.getName(), expected), + expected.contains(pokemon)); + } + } + } diff --git a/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/EntityManagerFactoryDelegate.java b/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/EntityManagerFactoryDelegate.java index ecf6e031a0f..1b1e986983f 100644 --- a/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/EntityManagerFactoryDelegate.java +++ b/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/EntityManagerFactoryDelegate.java @@ -159,6 +159,9 @@ public class EntityManagerFactoryDelegate implements EntityManagerFactory, Persi /** Pointer to the EntityManagerFactoryImpl that created me */ protected JpaEntityManagerFactory owner = null; + /** Persistence unit schema manager. */ + private SchemaManagerImpl schemaManager = null; + /** * Will return an instance of the Factory. Should only be called by * EclipseLink. @@ -564,10 +567,12 @@ public PersistenceUnitTransactionType getTransactionType() { }; } - // TODO-API-3.2 @Override public SchemaManager getSchemaManager() { - throw new UnsupportedOperationException("Jakarta Persistence 3.2 API was not implemented yet"); + if (schemaManager == null) { + schemaManager = new SchemaManagerImpl(getDatabaseSession()); + } + return schemaManager; } /** diff --git a/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/SchemaManagerImpl.java b/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/SchemaManagerImpl.java new file mode 100644 index 00000000000..906933ace17 --- /dev/null +++ b/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/SchemaManagerImpl.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 1998, 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2023 IBM Corporation. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// 11/28/2023: Tomas Kraus +// - New Jakarta Persistence 3.2 Features +package org.eclipse.persistence.internal.jpa; + +import java.util.List; +import java.util.function.Consumer; + +import jakarta.persistence.SchemaManager; +import jakarta.persistence.SchemaValidationException; +import org.eclipse.persistence.internal.localization.ExceptionLocalization; +import org.eclipse.persistence.internal.sessions.DatabaseSessionImpl; +import org.eclipse.persistence.tools.schemaframework.TableValidationException; + +public class SchemaManagerImpl implements SchemaManager { + + private final org.eclipse.persistence.tools.schemaframework.SchemaManager schemaManager; + + SchemaManagerImpl(DatabaseSessionImpl session) { + schemaManager = new org.eclipse.persistence.tools.schemaframework.SchemaManager(session); + } + + @Override + public void create(boolean createSchemas) { + if (createSchemas) { + schemaManager.createDefaultTables(true); + } + } + + @Override + public void drop(boolean dropSchemas) { + if (dropSchemas) { + schemaManager.dropDefaultTables(); + } + } + + // TODO-API-3.2 + @Override + public void validate() throws SchemaValidationException { + ValidationFailure failures = new ValidationFailure(); + if (!schemaManager.validateDefaultTables(failures, true)) { + throw new SchemaValidationException( + ExceptionLocalization.buildMessage("schema_validation_failed"), + failures.result().toArray(new TableValidationException[failures.result().size()])); + } + } + + @Override + public void truncate() { + schemaManager.truncateDefaultTables(false); + } + + private static final class ValidationFailure implements Consumer> { + + private static List validationResult; + + private ValidationFailure() { + validationResult = null; + } + + @Override + public void accept(List failures) { + validationResult = failures; + } + + private List result() { + return validationResult; + } + + } + +}