diff --git a/herddb-core/src/main/java/herddb/core/TableManager.java b/herddb-core/src/main/java/herddb/core/TableManager.java index f557776a8..a2cc81d28 100644 --- a/herddb-core/src/main/java/herddb/core/TableManager.java +++ b/herddb-core/src/main/java/herddb/core/TableManager.java @@ -20,6 +20,7 @@ package herddb.core; +import static herddb.sql.JSQLParserPlanner.delimit; import static java.util.concurrent.TimeUnit.SECONDS; import herddb.codec.RecordSerializer; import herddb.core.PageSet.DataPageMetaData; @@ -1262,15 +1263,15 @@ private void executeForeignKeyConstraintsAsParentTable(Table childTable, DataAcc String query = parentForeignKeyQueries.computeIfAbsent(childTable.name + "." + fk.name + ".#" + delete, (l -> { if (fk.onDeleteAction == ForeignKeyDef.ACTION_CASCADE && delete) { StringBuilder q = new StringBuilder("DELETE FROM "); - q.append(childTable.tablespace); + q.append(delimit(childTable.tablespace)); q.append("."); - q.append(childTable.name); + q.append(delimit(childTable.name)); q.append(" WHERE "); for (int i = 0; i < fk.columns.length; i++) { if (i > 0) { q.append(" AND "); } - q.append(fk.columns[i]); + q.append(delimit(fk.columns[i])); q.append("=?"); } return q.toString(); @@ -1282,15 +1283,15 @@ private void executeForeignKeyConstraintsAsParentTable(Table childTable, DataAcc } else if ((fk.onDeleteAction == ForeignKeyDef.ACTION_SETNULL && delete) || (fk.onUpdateAction == ForeignKeyDef.ACTION_SETNULL && !delete)) { // delete or update it is the same for SET NULL StringBuilder q = new StringBuilder("UPDATE "); - q.append(childTable.tablespace); + q.append(delimit(childTable.tablespace)); q.append("."); - q.append(childTable.name); + q.append(delimit(childTable.name)); q.append(" SET "); for (int i = 0; i < fk.columns.length; i++) { if (i > 0) { q.append(","); } - q.append(fk.columns[i]); + q.append(delimit(fk.columns[i])); q.append("= NULL "); } q.append(" WHERE "); @@ -1298,7 +1299,7 @@ private void executeForeignKeyConstraintsAsParentTable(Table childTable, DataAcc if (i > 0) { q.append(" AND "); } - q.append(fk.columns[i]); + q.append(delimit(fk.columns[i])); q.append("=?"); } return q.toString(); @@ -1306,15 +1307,15 @@ private void executeForeignKeyConstraintsAsParentTable(Table childTable, DataAcc // NO ACTION case, check that there is no matching record in the child table that wouble be invalidated // with '*' we are not going to perform projections or copies StringBuilder q = new StringBuilder("SELECT * FROM "); - q.append(childTable.tablespace); + q.append(delimit(childTable.tablespace)); q.append("."); - q.append(childTable.name); + q.append(delimit(childTable.name)); q.append(" WHERE "); for (int i = 0; i < fk.columns.length; i++) { if (i > 0) { q.append(" AND "); } - q.append(fk.columns[i]); + q.append(delimit(fk.columns[i])); q.append("=?"); } return q.toString(); @@ -1363,20 +1364,24 @@ private void executeForeignKeyConstraintsAsParentTable(Table childTable, DataAcc } private void validateForeignKeyConsistency(ForeignKeyDef fk, StatementEvaluationContext context, Transaction transaction) throws StatementExecutionException { + if (!tableSpaceManager.getDbmanager().isFullSQLSupportEnabled()) { + // we cannot perform this validation without Calcite + return; + } Table parentTable = tableSpaceManager.getTableManagerByUUID(fk.parentTableId).getTable(); StringBuilder query = new StringBuilder("SELECT * " - + " FROM " + this.tableSpaceManager.getTableSpaceName() + "." + this.table.name + " childtable " + + " FROM " + delimit(this.tableSpaceManager.getTableSpaceName()) + "." + delimit(this.table.name) + " childtable " + " WHERE NOT EXISTS (SELECT * " - + " FROM " + this.tableSpaceManager.getTableSpaceName() + "." + parentTable.name + " parenttable " + + " FROM " + delimit(this.tableSpaceManager.getTableSpaceName()) + "." + delimit(parentTable.name) + " parenttable " + " WHERE "); for (int i = 0; i < fk.columns.length; i++) { if (i > 0) { query.append(" AND "); } query.append("childtable.") - .append(fk.columns[i]) + .append(delimit(fk.columns[i])) .append(" = parenttable.") - .append(fk.parentTableColumns[i]); + .append(delimit(fk.parentTableColumns[i])); } query.append(")"); TransactionContext tx = transaction != null ? new TransactionContext(transaction.transactionId) : TransactionContext.NO_TRANSACTION; @@ -1409,15 +1414,15 @@ private void checkForeignKeyConstraintsAsChildTable(ForeignKeyDef fk, DataAccess Table parentTable = tableSpaceManager.getTableManagerByUUID(fk.parentTableId).getTable(); // with '*' we are not going to perform projections or copies StringBuilder q = new StringBuilder("SELECT * FROM "); - q.append(parentTable.tablespace); + q.append(delimit(parentTable.tablespace)); q.append("."); - q.append(parentTable.name); + q.append(delimit(parentTable.name)); q.append(" WHERE "); for (int i = 0; i < fk.parentTableColumns.length; i++) { if (i > 0) { q.append(" AND "); } - q.append(fk.parentTableColumns[i]); + q.append(delimit(fk.parentTableColumns[i])); q.append("=?"); } return q.toString(); diff --git a/herddb-core/src/main/java/herddb/core/system/SysforeignkeysTableManager.java b/herddb-core/src/main/java/herddb/core/system/SysforeignkeysTableManager.java index 8b6b340dd..71c6d5ad7 100644 --- a/herddb-core/src/main/java/herddb/core/system/SysforeignkeysTableManager.java +++ b/herddb-core/src/main/java/herddb/core/system/SysforeignkeysTableManager.java @@ -80,7 +80,36 @@ protected Iterable buildVirtualRecordList(Transaction transaction) { String child_column_name = fk.columns[i]; String parent_column_name = fk.parentTableColumns[i]; String parent_table_name = parent.name; - + String on_delete_action; + switch (fk.onDeleteAction) { + case ForeignKeyDef.ACTION_CASCADE: + on_delete_action = "importedKeyCascade"; + break; + case ForeignKeyDef.ACTION_NO_ACTION: + on_delete_action = "importedNoAction"; + break; + case ForeignKeyDef.ACTION_SETNULL: + on_delete_action = "importedKeySetNull"; + break; + default: + on_delete_action = "importedKeyCascade"; + break; + } + String on_update_action; + switch (fk.onUpdateAction) { + case ForeignKeyDef.ACTION_CASCADE: + on_update_action = "importedKeyCascade"; + break; + case ForeignKeyDef.ACTION_NO_ACTION: + on_update_action = "importedNoAction"; + break; + case ForeignKeyDef.ACTION_SETNULL: + on_update_action = "importedKeySetNull"; + break; + default: + on_update_action = "importedKeyCascade"; + break; + } result.add(RecordSerializer.makeRecord( table, "child_table_name", child_table_name, @@ -88,8 +117,8 @@ protected Iterable buildVirtualRecordList(Transaction transaction) { "child_table_cons_name", fk.name, "parent_table_name", parent_table_name, "parent_table_column_name", parent_column_name, - "on_delete_action", "importedNoAction", - "on_update_action", "importedNoAction", + "on_delete_action", on_delete_action, + "on_update_action", on_update_action, "ordinal_position", (i + 1), "deferred", "importedKeyNotDeferrable" )); diff --git a/herddb-core/src/main/java/herddb/sql/JSQLParserPlanner.java b/herddb-core/src/main/java/herddb/sql/JSQLParserPlanner.java index ddca36bb0..d2dd2749c 100644 --- a/herddb-core/src/main/java/herddb/sql/JSQLParserPlanner.java +++ b/herddb-core/src/main/java/herddb/sql/JSQLParserPlanner.java @@ -166,6 +166,13 @@ public class JSQLParserPlanner extends AbstractSQLPlanner { public static final String TABLE_CONSISTENCY_COMMAND = "tableconsistencycheck"; public static final String TABLESPACE_CONSISTENCY_COMMAND = "tablespaceconsistencycheck"; + + public static String delimit(String name) { + if (name == null) { + return null; + } + return "`" + name + "`"; + } private final PlansCache cache; /** * Used in case of unsupported Statement @@ -312,6 +319,10 @@ public TranslatedQuery translate( } query = rewriteExecuteSyntax(query); + if (query.startsWith("ALTER TABLE") && query.contains("ADD FOREIGN KEY")) { + // jsqlparser does not support unnamed foreign keys in "ALTER TABLE" + query = query.replace("ADD FOREIGN KEY", "ADD CONSTRAINT generate_unnamed FOREIGN KEY"); + } if (query.startsWith("EXPLAIN ")) { query = query.substring("EXPLAIN ".length()); net.sf.jsqlparser.statement.Statement stmt = parseStatement(query); @@ -601,6 +612,9 @@ private Statement buildCreateTableStatement(String defaultTableSpace, CreateTabl private ForeignKeyDef parseForeignKeyIndex(ForeignKeyIndex fk, Table table, String tableName, String tableSpace) throws StatementExecutionException { String indexName = fixMySqlBackTicks(fk.getName().toLowerCase()); + if (indexName.equals("generate_unnamed")) { + indexName = "fk_" + tableName + "_" + System.nanoTime(); + } int onUpdateCascadeAction = parseForeignKeyAction(fk.getOnUpdateReferenceOption()); int onDeleteCascadeAction = parseForeignKeyAction(fk.getOnDeleteReferenceOption()); Table parentTableSchema = getTable(table.tablespace, fk.getTable()); diff --git a/herddb-core/src/test/java/herddb/sql/ForeignKeySQLTest.java b/herddb-core/src/test/java/herddb/sql/ForeignKeySQLTest.java index 7540b59b1..fab9ad857 100644 --- a/herddb-core/src/test/java/herddb/sql/ForeignKeySQLTest.java +++ b/herddb-core/src/test/java/herddb/sql/ForeignKeySQLTest.java @@ -480,4 +480,31 @@ public void createTableWithOnUpdateSetNull() throws Exception { } } + @Test + public void alterAddUnnamedForeignKey() throws Exception { + String nodeId = "localhost"; + try (DBManager manager = new DBManager("localhost", new MemoryMetadataStorageManager(), new MemoryDataStorageManager(), new MemoryCommitLogManager(), null, null)) { + manager.start(); + CreateTableSpaceStatement st1 = new CreateTableSpaceStatement("tblspace1", Collections.singleton(nodeId), nodeId, 1, 0, 0); + manager.executeStatement(st1, StatementEvaluationContext.DEFAULT_EVALUATION_CONTEXT(), TransactionContext.NO_TRANSACTION); + manager.waitForTablespace("tblspace1", 10000); + + execute(manager, "CREATE TABLE tblspace1.parent (k1 string primary key,n1 int,s1 string)", Collections.emptyList()); + execute(manager, "CREATE TABLE tblspace1.child (k2 string primary key,n2 int," + + "s2 string)", Collections.emptyList()); + execute(manager, "ALTER TABLE tblspace1.child ADD FOREIGN KEY (s2,n2) REFERENCES parent(k1,n1)", Collections.emptyList()); + Table parentTable = manager.getTableSpaceManager("tblspace1").getTableManager("parent").getTable(); + Table childTable = manager.getTableSpaceManager("tblspace1").getTableManager("child").getTable(); + assertEquals(1, childTable.foreignKeys.length); + assertEquals(ForeignKeyDef.ACTION_NO_ACTION, childTable.foreignKeys[0].onUpdateAction); + assertEquals(ForeignKeyDef.ACTION_NO_ACTION, childTable.foreignKeys[0].onDeleteAction); + assertEquals(parentTable.uuid, childTable.foreignKeys[0].parentTableId); + assertArrayEquals(new String[]{"s2", "n2"}, childTable.foreignKeys[0].columns); + assertArrayEquals(new String[]{"k1", "n1"}, childTable.foreignKeys[0].parentTableColumns); + + // test FK is working + testChildSideOfForeignKey(manager, TransactionContext.NOTRANSACTION_ID, childTable.foreignKeys[0].name); + } + } + } diff --git a/herddb-jdbc/src/main/java/herddb/jdbc/HerdDBDatabaseMetadata.java b/herddb-jdbc/src/main/java/herddb/jdbc/HerdDBDatabaseMetadata.java index 8bbba71e3..f763586dc 100644 --- a/herddb-jdbc/src/main/java/herddb/jdbc/HerdDBDatabaseMetadata.java +++ b/herddb-jdbc/src/main/java/herddb/jdbc/HerdDBDatabaseMetadata.java @@ -1237,12 +1237,12 @@ private ResultSet getForeignKeysQueryResults(String query, String schema, String data.put("FKCOLUMN_NAME", parent_table_column_name); data.put("KEY_SEQ", ordinal_position); - data.put("UPDATE_RULE", on_update_action); - data.put("DELETE_RULE", on_delete_action); + data.put("UPDATE_RULE", convertFkActions(on_update_action)); + data.put("DELETE_RULE", convertFkActions(on_delete_action)); data.put("FK_NAME", child_table_cons_name); data.put("PK_NAME", null); - data.put("DEFERRABILITY", deferred); + data.put("DEFERRABILITY", convertDeferrability(deferred)); results.add(data); } @@ -1757,4 +1757,34 @@ public boolean isWrapperFor(Class iface) throws SQLException { return iface.isInstance(this); } + public static int convertFkActions(String action) { + switch (action + "") { + case "importedNoAction": + return importedKeyNoAction; + case "importedKeyCascade": + return importedKeyCascade; + case "importedKeySetNull": + return importedKeySetNull; + case "importedKeySetDefault": + return importedKeySetDefault; + case "importedKeyRestrict": + return importedKeyRestrict; + default: + return importedKeyNoAction; + } + } + + private static int convertDeferrability(String deferred) { + switch (deferred + "") { + case "importedKeyNotDeferrable": + return importedKeyNotDeferrable; + case "importedKeyInitiallyDeferred": + return importedKeyInitiallyDeferred; + case "importedKeyInitiallyImmediate": + return importedKeyInitiallyImmediate; + default: + return importedKeyNotDeferrable; + } + } + } diff --git a/herddb-jdbc/src/test/java/herddb/jdbc/JdbcForeignKeyMetadataTest.java b/herddb-jdbc/src/test/java/herddb/jdbc/JdbcForeignKeyMetadataTest.java index baab736ff..82b9e6d80 100644 --- a/herddb-jdbc/src/test/java/herddb/jdbc/JdbcForeignKeyMetadataTest.java +++ b/herddb-jdbc/src/test/java/herddb/jdbc/JdbcForeignKeyMetadataTest.java @@ -52,7 +52,7 @@ public void test() throws Exception { Statement statement = con.createStatement()) { statement.execute("CREATE TABLE ptable (pkey string primary key, p1 string, p2 string)"); statement.execute("CREATE TABLE ctable (ckey string primary key, c1 string, c2 string," - + " CONSTRAINT `fk1` FOREIGN KEY (`c1`,`c2`) REFERENCES ptable(`p1`,`p2`))"); + + " CONSTRAINT `fk1` FOREIGN KEY (`c1`,`c2`) REFERENCES ptable(`p1`,`p2`) ON DELETE CASCADE)"); DatabaseMetaData metaData = con.getMetaData(); try (ResultSet rs = metaData.getImportedKeys(null, null, "CTABLE");) { verifyForeignKeyResultSet(rs); @@ -100,7 +100,7 @@ private void verifyForeignKeyResultSet(final ResultSet importedKeys) throws SQLE assertEquals("fk1", importedKeys.getString("FK_NAME")); assertEquals(null, importedKeys.getString("PK_NAME")); - assertEquals("importedKeyNotDeferrable", importedKeys.getString("DEFERRABILITY")); + assertEquals(DatabaseMetaData.importedKeyNotDeferrable, importedKeys.getInt("DEFERRABILITY")); if (count == 0) { assertEquals("c1", importedKeys.getString("PKCOLUMN_NAME")); assertEquals("p1", importedKeys.getString("FKCOLUMN_NAME")); @@ -108,8 +108,8 @@ private void verifyForeignKeyResultSet(final ResultSet importedKeys) throws SQLE assertEquals("c2", importedKeys.getString("PKCOLUMN_NAME")); assertEquals("p2", importedKeys.getString("FKCOLUMN_NAME")); } - assertEquals("importedNoAction", importedKeys.getString("UPDATE_RULE")); - assertEquals("importedNoAction", importedKeys.getString("DELETE_RULE")); + assertEquals(DatabaseMetaData.importedKeyNoAction, importedKeys.getInt("UPDATE_RULE")); + assertEquals(DatabaseMetaData.importedKeyCascade, importedKeys.getInt("DELETE_RULE")); count++; } assertEquals(2, count); diff --git a/herddb-thirdparty/openjpa-test/nb-configuration.xml b/herddb-thirdparty/openjpa-test/nb-configuration.xml new file mode 100644 index 000000000..ec4540cb4 --- /dev/null +++ b/herddb-thirdparty/openjpa-test/nb-configuration.xml @@ -0,0 +1,18 @@ + + + + + + none + + diff --git a/herddb-thirdparty/openjpa-test/pom.xml b/herddb-thirdparty/openjpa-test/pom.xml index bf6ae15a9..1da9a7295 100644 --- a/herddb-thirdparty/openjpa-test/pom.xml +++ b/herddb-thirdparty/openjpa-test/pom.xml @@ -40,8 +40,8 @@ org.apache.openjpa - openjpa - 3.1.1 + openjpa-persistence-jdbc + 3.1.2 + test.entity.User + test.entity.Address true + + diff --git a/herddb-thirdparty/openjpa-test/src/test/java/test/DataSourceTest.java b/herddb-thirdparty/openjpa-test/src/test/java/test/DataSourceTest.java index 65f5b52f9..812ee2d22 100644 --- a/herddb-thirdparty/openjpa-test/src/test/java/test/DataSourceTest.java +++ b/herddb-thirdparty/openjpa-test/src/test/java/test/DataSourceTest.java @@ -27,6 +27,7 @@ import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import org.junit.Test; +import test.entity.Address; import test.entity.User; /** @@ -51,22 +52,48 @@ public void hello() throws Exception { final EntityManagerFactory factory = Persistence.createEntityManagerFactory("hdb_ds", Map.of("openjpa.ConnectionFactory", ds2)); + performAssertions(factory); + + factory.close(); + + ds2.close(); + + } + + static void performAssertions(final EntityManagerFactory factory) { { final EntityManager em = factory.createEntityManager(); final EntityTransaction transaction = em.getTransaction(); transaction.begin(); + Address a = new Address(0, "Localhost"); + em.persist(a); em.persist(new User(0, "First", 10, "Something")); + // Uncomment when we are on OpenJPA 3.1.3 + // em.persist(new User(0, "First", 10, "Something", a)); transaction.commit(); em.close(); } { final EntityManager em = factory.createEntityManager(); assertEquals(1, em.createQuery("select e from User e").getResultList().size()); + assertEquals(1, em.createQuery("select e from Address e").getResultList().size()); em.close(); } - factory.close(); - - ds2.close(); + { + final EntityManager em = factory.createEntityManager(); + final EntityTransaction transaction = em.getTransaction(); + transaction.begin(); + em.createQuery("DELETE from User e").executeUpdate(); + transaction.commit(); + em.close(); + } +// Uncomment when we are on OpenJPA 3.1.3 +// { +// final EntityManager em = factory.createEntityManager(); +// assertEquals(0, em.createQuery("select e from User e").getResultList().size()); +// assertEquals(0, em.createQuery("select e from Address e").getResultList().size()); +// em.close(); +// } } } diff --git a/herddb-thirdparty/openjpa-test/src/test/java/test/JDBCDriverTest.java b/herddb-thirdparty/openjpa-test/src/test/java/test/JDBCDriverTest.java index 603e123e0..273114f92 100644 --- a/herddb-thirdparty/openjpa-test/src/test/java/test/JDBCDriverTest.java +++ b/herddb-thirdparty/openjpa-test/src/test/java/test/JDBCDriverTest.java @@ -19,16 +19,12 @@ */ package test; -import static org.junit.Assert.assertEquals; import java.sql.Driver; import java.sql.DriverManager; import java.util.Enumeration; -import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; -import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import org.junit.Test; -import test.entity.User; /** * @@ -42,19 +38,8 @@ public void hello() throws Exception { final EntityManagerFactory factory = Persistence.createEntityManagerFactory("hdb_jdbc"); - { - final EntityManager em = factory.createEntityManager(); - final EntityTransaction transaction = em.getTransaction(); - transaction.begin(); - em.persist(new User(0, "First", 10, "Something")); - transaction.commit(); - em.close(); - } - { - final EntityManager em = factory.createEntityManager(); - assertEquals(1, em.createQuery("select e from User e").getResultList().size()); - em.close(); - } + DataSourceTest.performAssertions(factory); + factory.close(); // clean up, unregistring the driver will shutdown every datasource diff --git a/herddb-thirdparty/openjpa/pom.xml b/herddb-thirdparty/openjpa/pom.xml index ac2afecec..ede9ea3e1 100644 --- a/herddb-thirdparty/openjpa/pom.xml +++ b/herddb-thirdparty/openjpa/pom.xml @@ -34,8 +34,8 @@ org.apache.openjpa - openjpa - 3.1.1 + openjpa-jdbc + 3.1.2 commons-dbcp diff --git a/herddb-thirdparty/openjpa/src/main/java/herddb/openjpa/DBDictionary.java b/herddb-thirdparty/openjpa/src/main/java/herddb/openjpa/DBDictionary.java index 17019bd70..5671870b9 100644 --- a/herddb-thirdparty/openjpa/src/main/java/herddb/openjpa/DBDictionary.java +++ b/herddb-thirdparty/openjpa/src/main/java/herddb/openjpa/DBDictionary.java @@ -28,11 +28,11 @@ public class DBDictionary extends org.apache.openjpa.jdbc.sql.DBDictionary { public DBDictionary() { platform = "HerdDB"; databaseProductName = "HerdDB"; - supportsForeignKeys = false; - supportsUniqueConstraints = false; - supportsCascadeDeleteAction = false; + supportsDeferredConstraints = false; + supportsCascadeUpdateAction = false; schemaCase = SCHEMA_CASE_LOWER; delimitedCase = SCHEMA_CASE_PRESERVE; +// useSchemaName = false; // make OpenJPA escape everything, because Apache Calcite has a lot of reserved words, like 'User', 'Value'... setDelimitIdentifiers(true);