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..c19af4fc7 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.3-SNAPSHOT