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..cd1623082a1 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,9 @@ 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 vaw not found in the schema"} }; /** * Return the lookup table. 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..cbbcfa7f076 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 fastReplaceDefaultTables(boolean generateFKConstraints) { + boolean shouldLogExceptionStackTrace = getSession().getSessionLog().shouldLogExceptionStackTrace(); + session.getSessionLog().setShouldLogExceptionStackTrace(false); + + try { + TableCreator tableCreator = getDefaultTableCreator(generateFKConstraints); + tableCreator.fastReplaceTables(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..cbf8cbe31d7 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 @@ -35,6 +35,7 @@ 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 +352,24 @@ public void replaceTables(DatabaseSession session, SchemaManager schemaManager, replaceTablesAndConstraints(schemaManager, session, createSequenceTables, createSequences); } + void fastReplaceTables(DatabaseSession session, SchemaManager schemaManager, boolean generateFKConstraints) { + TableCreator tableCreator = schemaManager.getDefaultTableCreator(generateFKConstraints); + String sequenceTableName = tableCreator.getSequenceTableName(session); + List tables = tableCreator.getTableDefinitions(); + for (TableDefinition table : tables) { + if (!table.getName().equals(sequenceTableName)) { + try { + // TODO: JPQL DELETE would be better (slightly slower, but DB independent) + schemaManager.getSession() + .priviledgedExecuteNonSelectingCall( + new org.eclipse.persistence.queries.SQLCall("DELETE FROM " + table.getFullName())); + } catch (DatabaseException ex) { + //Ignore database exception. eg. If there is no table to delete, it gives database exception. + } + } + } + } + protected void replaceTablesAndConstraints(SchemaManager schemaManager, DatabaseSession session, boolean createSequenceTables, boolean createSequences) { buildConstraints(schemaManager, true); boolean ignore = shouldIgnoreDatabaseException(); @@ -442,6 +461,46 @@ 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)) { + // TODO: Check table content + List columnInfo = readColumnInfo((AbstractSession) session, tableDefinition); + if (columnInfo != null && !columnInfo.isEmpty()) { + + } + } else { + exceptions.add(new TableValidationException.Missing(tableName)); + } + }); + if (exceptions.isEmpty()) { + return true; + } else { + // Pass validation failures to provided consumer + onFailed.accept(exceptions); + return false; + } + } + /** * This creates/extends the tables on the database. * @param session Active database session. @@ -477,20 +536,9 @@ public void extendTables(final DatabaseSession session, final SchemaManager sche //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 @@ -575,4 +623,22 @@ 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; + } + } 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..7302805f50b --- /dev/null +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/tools/schemaframework/TableValidationException.java @@ -0,0 +1,40 @@ +package org.eclipse.persistence.tools.schemaframework; + +import jakarta.persistence.SchemaValidationException; +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 static final class Missing extends TableValidationException { + + Missing(String table) { + super(ExceptionLocalization.buildMessage( + "schema_validation_missing_table", new String[] {table}), + table, Type.MISSING); + } + + } + + public enum Type { + /** Missing table in the schema. */ + MISSING; + } + + +} 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..97706c02bb0 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 @@ -87,6 +87,8 @@ static TestSuite suite(String name, AbstractPokemon... tests) { return suite; } + private Persistence32TableCreator tableCreator = null; + JpaEntityManagerFactory emf = null; public AbstractPokemon() { @@ -105,6 +107,7 @@ public String getPersistenceUnitName() { @Override public void setUp() { super.setUp(); + tableCreator = new Persistence32TableCreator(); emf = getEntityManagerFactory(getPersistenceUnitName()) .unwrap(EntityManagerFactoryImpl.class); } @@ -128,7 +131,7 @@ Map pokemonTypes(EntityManager em) { * execution in the server. */ public void testSetup() { - new Persistence32TableCreator().replaceTables(JUnitTestCase.getServerSession(getPersistenceUnitName())); + tableCreator.replaceTables(JUnitTestCase.getServerSession(getPersistenceUnitName())); emf.runInTransaction(em -> { for (int i = 1; i < TEAMS.length; i++) { em.persist(TEAMS[i]); 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..38b46d70b3b --- /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,107 @@ +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.jpa.junit.JUnitTestCase; + +/** + * 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. + * 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 + */ + static TestSuite suite(String name, AbstractSchemaManager... tests) { + TestSuite suite = new TestSuite(); + suite.setName(name); + for (AbstractSchemaManager test : tests) { + suite.addTest(test); + } + return suite; + } + + 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/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..68563af4326 --- /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,61 @@ +/* + * 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; + +/** + * 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..f36e271d7fd --- /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,106 @@ +/* + * 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; + +/** + * 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..e16144e13e9 --- /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,73 @@ +/* + * 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; + +/** + * 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/SchemaManagerValidateOnMissingTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnMissingTest.java new file mode 100644 index 00000000000..30ef4af8a6e --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnMissingTest.java @@ -0,0 +1,70 @@ +/* + * 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; + +/** + * Test {@link SchemaManager#validate()} method on database with missing schema. + */ +public class SchemaManagerValidateOnMissingTest extends AbstractSchemaManager { + + public static Test suite() { + return suite( + "SchemaManagerValidateOnMissingTest", + new SchemaManagerValidateOnMissingTest("testValidateOnMissingSchema") + ); + } + public SchemaManagerValidateOnMissingTest() { + } + + public SchemaManagerValidateOnMissingTest(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) { + 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/SchemaManagerValidateOnValidTest.java b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnValidTest.java new file mode 100644 index 00000000000..d7627108597 --- /dev/null +++ b/jpa/eclipselink.jpa.testapps/jpa.test.persistence32/src/test/java/org/eclipse/persistence/testing/tests/jpa/persistence32/SchemaManagerValidateOnValidTest.java @@ -0,0 +1,49 @@ +/* + * 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; + +/** + * Test {@link SchemaManager#validate()} method on database with existing and valid schema. + */ +public class SchemaManagerValidateOnValidTest extends AbstractSchemaManager { + + public static Test suite() { + return suite( + "SchemaManagerValidateOnValidTest", + new SchemaManagerValidateOnValidTest("testValidateOnValidSchema") + ); + } + public SchemaManagerValidateOnValidTest() { + } + + public SchemaManagerValidateOnValidTest(String name) { + super(name); + } + + // Test SchemaManager validate method on existing valid schema + public void testValidateOnValidSchema() { + // Make sure that tables exist before being dropped + createTables(); + SchemaManager schemaManager = emf.getSchemaManager(); + try { + // Test validation + schemaManager.validate(); + } catch (SchemaValidationException sve) { + fail(sve.getLocalizedMessage()); + } + } + +} 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..064eef46845 --- /dev/null +++ b/jpa/org.eclipse.persistence.jpa/src/main/java/org/eclipse/persistence/internal/jpa/SchemaManagerImpl.java @@ -0,0 +1,89 @@ +/* + * 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.exceptions.DatabaseException; +import org.eclipse.persistence.internal.localization.ExceptionLocalization; +import org.eclipse.persistence.internal.sessions.AbstractSession; +import org.eclipse.persistence.internal.sessions.DatabaseSessionImpl; +import org.eclipse.persistence.tools.schemaframework.TableCreator; +import org.eclipse.persistence.tools.schemaframework.TableDefinition; +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.fastReplaceDefaultTables(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; + } + + } + +}