diff --git a/migration/iD11/oracle/202304280940_IDEMPIERE-5683.sql b/migration/iD11/oracle/202304280940_IDEMPIERE-5683.sql new file mode 100644 index 0000000000..0c5cd8e859 --- /dev/null +++ b/migration/iD11/oracle/202304280940_IDEMPIERE-5683.sql @@ -0,0 +1,33 @@ +-- IDEMPIERE-5683 +SELECT register_migration_script('202304280940_IDEMPIERE-5683.sql') FROM dual; + +SET SQLBLANKLINES ON +SET DEFINE OFF + +-- Apr 28, 2023, 9:41:24 AM CEST +UPDATE AD_Field SET DisplayLogic='@AD_Reference_ID@=19 | @AD_Reference_ID@=30 | @AD_Reference_ID@=18 | @AD_Reference_ID@=21 | @AD_Reference_ID@=25 | @AD_Reference_ID@=31 | @AD_Reference_ID@=35 | @AD_Reference_ID@=33 | @AD_Reference_ID@=32 | @AD_Reference_ID@=53370 | @AD_Reference_ID@=200233 | @AD_Reference_ID@=200234 | @AD_Reference_ID@=200235 | @AD_Reference_ID@=200202 | @AD_Reference_ID@=200240',Updated=TO_TIMESTAMP('2023-04-28 09:41:24','YYYY-MM-DD HH24:MI:SS'),UpdatedBy=100 WHERE AD_Field_ID=202519 +; + +-- update Constraint Type based on java arrays from PO_Record.java +UPDATE AD_Column +SET FKConstraintType = + CASE + WHEN AD_Table_ID IN(254,754,389,200000,200215,200347) THEN 'C' /* AD_Attachment, AD_Archive, AD_Note, AD_RecentItem, AD_PostIt, AD_LabelAssignment - Cascade */ + WHEN AD_Table_ID IN(417,876) THEN 'N' /* R_Request, CM_Chat - No Action */ + ELSE 'D' + END +WHERE AD_Reference_ID IN(200202,200240) /* Record ID, Record UU */ +; + +-- May 4, 2023, 5:28:36 PM CEST +UPDATE AD_Val_Rule SET Code='( +(AD_Ref_List.Value!=''M'' OR @AD_Reference_ID@ IN (18,19,30,200233,200234,200235,200202,200240)) +AND +((AD_Ref_List.Value=''C'' AND @AD_Reference_ID@ NOT IN (200202,200240)) OR (AD_Ref_List.Value!=''C'')) +)',Updated=TO_TIMESTAMP('2023-05-04 17:28:36','YYYY-MM-DD HH24:MI:SS'),UpdatedBy=100 WHERE AD_Val_Rule_ID=200064 +; + +-- May 5, 2023, 8:15:59 AM CEST +DELETE FROM AD_Process_Para WHERE AD_Process_Para_UU='459a7f88-ec79-47cf-9c7b-7429ac565f55' +; + diff --git a/migration/iD11/postgresql/202304280940_IDEMPIERE-5683.sql b/migration/iD11/postgresql/202304280940_IDEMPIERE-5683.sql new file mode 100644 index 0000000000..eeeb88cdd4 --- /dev/null +++ b/migration/iD11/postgresql/202304280940_IDEMPIERE-5683.sql @@ -0,0 +1,30 @@ +-- IDEMPIERE-5683 +SELECT register_migration_script('202304280940_IDEMPIERE-5683.sql') FROM dual; + +-- Apr 28, 2023, 9:41:24 AM CEST +UPDATE AD_Field SET DisplayLogic='@AD_Reference_ID@=19 | @AD_Reference_ID@=30 | @AD_Reference_ID@=18 | @AD_Reference_ID@=21 | @AD_Reference_ID@=25 | @AD_Reference_ID@=31 | @AD_Reference_ID@=35 | @AD_Reference_ID@=33 | @AD_Reference_ID@=32 | @AD_Reference_ID@=53370 | @AD_Reference_ID@=200233 | @AD_Reference_ID@=200234 | @AD_Reference_ID@=200235 | @AD_Reference_ID@=200202 | @AD_Reference_ID@=200240',Updated=TO_TIMESTAMP('2023-04-28 09:41:24','YYYY-MM-DD HH24:MI:SS'),UpdatedBy=100 WHERE AD_Field_ID=202519 +; + +-- update Constraint Type based on java arrays from PO_Record.java +UPDATE AD_Column +SET FKConstraintType = + CASE + WHEN AD_Table_ID IN(254,754,389,200000,200215,200347) THEN 'C' /* AD_Attachment, AD_Archive, AD_Note, AD_RecentItem, AD_PostIt, AD_LabelAssignment - Cascade */ + WHEN AD_Table_ID IN(417,876) THEN 'N' /* R_Request, CM_Chat - No Action */ + ELSE 'D' + END +WHERE AD_Reference_ID IN(200202,200240) /* Record ID, Record UU */ +; + +-- May 4, 2023, 5:28:36 PM CEST +UPDATE AD_Val_Rule SET Code='( +(AD_Ref_List.Value!=''M'' OR @AD_Reference_ID@ IN (18,19,30,200233,200234,200235,200202,200240)) +AND +((AD_Ref_List.Value=''C'' AND @AD_Reference_ID@ NOT IN (200202,200240)) OR (AD_Ref_List.Value!=''C'')) +)',Updated=TO_TIMESTAMP('2023-05-04 17:28:36','YYYY-MM-DD HH24:MI:SS'),UpdatedBy=100 WHERE AD_Val_Rule_ID=200064 +; + +-- May 5, 2023, 8:15:59 AM CEST +DELETE FROM AD_Process_Para WHERE AD_Process_Para_UU='459a7f88-ec79-47cf-9c7b-7429ac565f55' +; + diff --git a/org.adempiere.base.process/src/org/idempiere/process/CleanOrphanCascade.java b/org.adempiere.base.process/src/org/idempiere/process/CleanOrphanCascade.java index a27ee7feef..eb05130a7f 100644 --- a/org.adempiere.base.process/src/org/idempiere/process/CleanOrphanCascade.java +++ b/org.adempiere.base.process/src/org/idempiere/process/CleanOrphanCascade.java @@ -26,16 +26,15 @@ package org.idempiere.process; import java.math.BigDecimal; import java.util.List; +import java.util.Objects; import java.util.logging.Level; -import org.compiere.model.MArchive; -import org.compiere.model.MAttachment; -import org.compiere.model.MProcessPara; +import org.compiere.model.MColumn; import org.compiere.model.MTable; import org.compiere.model.MTree_Base; +import org.compiere.model.PO; import org.compiere.model.Query; import org.compiere.model.X_AD_Package_UUID_Map; -import org.compiere.process.ProcessInfoParameter; import org.compiere.process.SvrProcess; import org.compiere.util.DB; import org.compiere.util.Msg; @@ -49,22 +48,11 @@ import org.compiere.util.ValueNamePair; public class CleanOrphanCascade extends SvrProcess { - private boolean p_IsCleanChangeLog; - /** * Prepare - e.g., get Parameters. */ protected void prepare() { - for (ProcessInfoParameter para : getParameter()) - { - String name = para.getParameterName(); - if ("IsCleanChangeLog".equals(name)) { - p_IsCleanChangeLog = para.getParameterAsBoolean(); - } else { - MProcessPara.validateUnknownParameter(getProcessInfo().getAD_Process_ID(), para); - } - } } // prepare /** @@ -120,9 +108,6 @@ public class CleanOrphanCascade extends SvrProcess + " FROM AD_Column ck " + " WHERE ck.IsActive='Y' AND ck.AD_Table_ID = AD_Table.AD_Table_ID " + " AND ck.ColumnName = AD_Table.TableName || '_ID')"; - if (! p_IsCleanChangeLog) { - whereTables += " AND TableName != 'AD_ChangeLog'"; - } List tables = new Query(getCtx(), "AD_Table", whereTables, get_TrxName()) .setOnlyActiveRecords(true) @@ -134,16 +119,21 @@ public class CleanOrphanCascade extends SvrProcess boolean isUUIDMap = X_AD_Package_UUID_Map.Table_Name.equals(tableName); StringBuilder sqlRef = new StringBuilder(); - sqlRef.append("SELECT DISTINCT t.AD_Table_ID, "); - sqlRef.append(" t.TableName "); - sqlRef.append("FROM ").append(tableName).append(" r "); - sqlRef.append(" JOIN AD_Table t ON ( r.AD_Table_ID = t.AD_Table_ID ) "); - sqlRef.append("ORDER BY t.Tablename"); + sqlRef.append("SELECT DISTINCT t.AD_Table_ID, ") + .append(" t.TableName, ") + .append(" c.FKConstraintType, ") + .append(" c.IsMandatory ") + .append("FROM ").append(tableName).append(" r ") + .append(" JOIN AD_Table t ON ( r.AD_Table_ID = t.AD_Table_ID ) ") + .append(" JOIN AD_Column c ON (t.AD_Table_ID = c.AD_Table_ID AND c.ColumnName = 'Record_ID') ") + .append("ORDER BY t.Tablename"); List> rowTables = DB.getSQLArrayObjectsEx(get_TrxName(), sqlRef.toString()); if (rowTables != null) { for (List row : rowTables) { int refTableID = ((BigDecimal) row.get(0)).intValue(); String refTableName = row.get(1).toString(); + String constraintType = Objects.toString(row.get(2), ""); + Boolean isMandatory = row.get(3).toString().equalsIgnoreCase("Y"); MTable refTable = MTable.get(getCtx(), refTableID); StringBuilder whereClause = new StringBuilder(); @@ -168,24 +158,19 @@ public class CleanOrphanCascade extends SvrProcess } int noDel = 0; - if (MAttachment.Table_Name.equals(tableName)) { - // special case for attachment because of store - List attachments = new Query(getCtx(), tableName, whereClause.toString(), get_TrxName()).list(); - for (MAttachment attachment : attachments) { - attachment.deleteEx(true, get_TrxName()); - noDel++; + List poList = new Query(getCtx(), tableName, whereClause.toString(), get_TrxName()).list(); + for (PO po : poList) { + if(MColumn.FKCONSTRAINTTYPE_ModelCascade.equals(constraintType)) { + po.deleteEx(true, get_TrxName()); } - } else if (MArchive.Table_Name.equals(tableName)) { - // special case for archive because of store - List archives = new Query(getCtx(), tableName, whereClause.toString(), get_TrxName()).list(); - for (MArchive archive : archives) { - archive.deleteEx(true, get_TrxName()); - noDel++; - } - } else { - StringBuilder sqlDelete = new StringBuilder(); - sqlDelete.append("DELETE FROM ").append(tableName).append(" WHERE ").append(whereClause); - noDel = DB.executeUpdateEx(sqlDelete.toString(), get_TrxName()); + else if(MColumn.FKCONSTRAINTTYPE_SetNull.equals(constraintType)) { + if(isMandatory) + po.set_ValueOfColumn("Record_ID", 0); + else + po.set_ValueOfColumn("Record_ID", null); + po.saveEx(get_TrxName()); + } + noDel++; } if (noDel > 0) { addLog(Msg.parseTranslation(getCtx(), noDel + " " + tableName + " " + "@Deleted@ -> " + refTableName)); @@ -199,14 +184,16 @@ public class CleanOrphanCascade extends SvrProcess } // doIt private void delTree(String treeTable, String foreignTable, String columnName, int treeId) { - StringBuilder sqlDelete = new StringBuilder() - .append("DELETE FROM ").append(treeTable) - .append(" WHERE ").append(columnName).append(">0 AND ") - .append(columnName).append(" NOT IN (SELECT ").append(foreignTable).append("_ID FROM ").append(foreignTable).append(")"); - if (treeId > 0) { - sqlDelete.append(" AND AD_Tree_ID=").append(treeId); + String whereClause = columnName + ">0 AND " + columnName + " NOT IN (SELECT " + foreignTable + "_ID FROM " + foreignTable + ")"; + if(treeId > 0) + whereClause += " AND AD_Tree_ID=" + treeId; + List poList = new Query(getCtx(), treeTable, whereClause, get_TrxName()).list(); + + int noDel = 0; + for(PO po : poList) { + po.deleteEx(true, get_TrxName()); + noDel++; } - int noDel = DB.executeUpdateEx(sqlDelete.toString(), get_TrxName()); if (noDel > 0) { addLog(Msg.parseTranslation(getCtx(), noDel + " " + treeTable + " " + "@Deleted@ -> " + foreignTable + (treeId > 0 ? " Tree=" + treeId: "" ))); diff --git a/org.adempiere.base/src/org/compiere/model/PO.java b/org.adempiere.base/src/org/compiere/model/PO.java index 56fbf8f091..50c6806c66 100644 --- a/org.adempiere.base/src/org/compiere/model/PO.java +++ b/org.adempiere.base/src/org/compiere/model/PO.java @@ -3972,7 +3972,10 @@ public abstract class PO if (m_KeyColumns != null && m_KeyColumns.length == 1) { PO_Record.deleteModelCascade(p_info.getTableName(), Record_ID, localTrxName); } - + + // Set referencing Record_ID Null AD_Table_ID/Record_ID + PO_Record.setRecordIdNull(AD_Table_ID, Record_ID, localTrxName); + // The Delete Statement String where = isLogSQLScript() ? get_WhereClause(true, get_ValueAsString(getUUIDColumnName())) : get_WhereClause(true); List optimisticLockingParams = new ArrayList(); diff --git a/org.adempiere.base/src/org/compiere/model/PO_Record.java b/org.adempiere.base/src/org/compiere/model/PO_Record.java index b90706d9e2..242a29513f 100644 --- a/org.adempiere.base/src/org/compiere/model/PO_Record.java +++ b/org.adempiere.base/src/org/compiere/model/PO_Record.java @@ -18,13 +18,16 @@ package org.compiere.model; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.util.ArrayList; import java.util.List; import java.util.logging.Level; +import org.compiere.util.CCache; import org.compiere.util.CLogger; import org.compiere.util.DB; import org.compiere.util.DisplayType; import org.compiere.util.Env; +import org.compiere.util.KeyNamePair; /** * Maintain AD_Table_ID/Record_ID constraint @@ -34,6 +37,9 @@ import org.compiere.util.Env; */ public class PO_Record { + /* Cache for arrays of KeyNamePair for types of deletion: Cascade, Set Null, No Action */ + private static final CCache s_po_record_tables_cache = new CCache<>(null, "PORecordTables", 3, 120, false, 3); + /** Parent Tables */ private static int[] s_parents = new int[]{ X_C_Order.Table_ID @@ -47,40 +53,6 @@ public class PO_Record private static String[] s_parentChildNames = new String[]{ X_C_OrderLine.Table_Name }; - - - - /** Cascade Table ID */ - private static int[] s_cascades = new int[]{ - X_AD_Attachment.Table_ID, - X_AD_Archive.Table_ID, - X_AD_Note.Table_ID, - X_AD_RecentItem.Table_ID, - X_AD_PostIt.Table_ID, - X_AD_LabelAssignment.Table_ID - }; - /** Cascade Table Names */ - private static String[] s_cascadeNames = new String[]{ - X_AD_Attachment.Table_Name, - X_AD_Archive.Table_Name, - X_AD_Note.Table_Name, - X_AD_RecentItem.Table_Name, - X_AD_PostIt.Table_Name, - X_AD_LabelAssignment.Table_Name - }; - - /** Restrict Table ID */ - private static int[] s_restricts = new int[]{ - X_R_Request.Table_ID, - X_CM_Chat.Table_ID - // X_Fact_Acct.Table_ID - }; - /** Restrict Table Names */ - private static String[] s_restrictNames = new String[]{ - X_R_Request.Table_Name, - X_CM_Chat.Table_Name - // X_Fact_Acct.Table_Name - }; /** Logger */ private static CLogger log = CLogger.getCLogger (PO_Record.class); @@ -94,60 +66,52 @@ public class PO_Record */ static boolean deleteCascade (int AD_Table_ID, int Record_ID, String trxName) { + KeyNamePair[] cascades = getTablesWithRecordColumnFromCache(MColumn.FKCONSTRAINTTYPE_Cascade); // Table Loop - for (int i = 0; i < s_cascades.length; i++) + for (KeyNamePair table : cascades) { // DELETE FROM table WHERE AD_Table_ID=#1 AND Record_ID=#2 - if (s_cascades[i] != AD_Table_ID) + if (table.getKey() != AD_Table_ID) { - Object[] params = new Object[]{Integer.valueOf(AD_Table_ID), Integer.valueOf(Record_ID)}; - if (s_cascadeNames[i].equals(X_AD_Attachment.Table_Name) || s_cascadeNames[i].equals(X_AD_Archive.Table_Name)) + List poList = new Query(Env.getCtx(), table.getName(), "AD_Table_ID=? AND Record_ID=?", trxName) + .setParameters(AD_Table_ID, Record_ID) + .list(); + + int count = 0; + for(PO po : poList) { - Query query = new Query(Env.getCtx(), s_cascadeNames[i], "AD_Table_ID=? AND Record_ID=?", trxName); - List list = query.setParameters(params).list(); - for(PO po : list) - { - po.deleteEx(true); - } - } - else - { - StringBuilder sql = new StringBuilder ("DELETE FROM ") - .append(s_cascadeNames[i]) - .append(" WHERE AD_Table_ID=? AND Record_ID=?"); - int no = DB.executeUpdate(sql.toString(), params, false, trxName); - if (no > 0) { - if (log.isLoggable(Level.CONFIG)) log.config(s_cascadeNames[i] + " (" + AD_Table_ID + "/" + Record_ID + ") #" + no); - } else if (no < 0) { - log.severe(s_cascadeNames[i] + " (" + AD_Table_ID + "/" + Record_ID + ") #" + no); - return false; - } + po.deleteEx(true); + count++; } + if (count > 0) + if (log.isLoggable(Level.CONFIG)) log.config(table.getName() + " (" + AD_Table_ID + "/" + Record_ID + ") #" + count); } } // Parent Loop - for (int j = 0; j < s_parents.length; j++) + for (int i = 0; i < s_parents.length; i++) { - if (s_parents[j] == AD_Table_ID) + if (s_parents[i] == AD_Table_ID) { - int AD_Table_IDchild = s_parentChilds[j]; - Object[] params = new Object[]{Integer.valueOf(AD_Table_IDchild), Integer.valueOf(Record_ID)}; - for (int i = 0; i < s_cascades.length; i++) + int AD_Table_IDchild = s_parentChilds[i]; + for (KeyNamePair table : cascades) { - StringBuilder sql = new StringBuilder ("DELETE FROM ") - .append(s_cascadeNames[i]) - .append(" WHERE AD_Table_ID=? AND Record_ID IN (SELECT ") - .append(s_parentChildNames[j]).append("_ID FROM ") - .append(s_parentChildNames[j]).append(" WHERE ") - .append(s_parentNames[j]).append("_ID=?)"); - int no = DB.executeUpdate(sql.toString(), params, false, trxName); - if (no > 0) { - if (log.isLoggable(Level.CONFIG)) log.config(s_cascadeNames[i] + " " + s_parentNames[j] - + " (" + AD_Table_ID + "/" + Record_ID + ") #" + no); - } else if (no < 0) { - log.severe(s_cascadeNames[i] + " " + s_parentNames[j] - + " (" + AD_Table_ID + "/" + Record_ID + ") #" + no); - return false; + String whereClause = " AD_Table_ID=? AND Record_ID IN (SELECT " + + s_parentChildNames[i] + "_ID FROM " + + s_parentChildNames[i] + " WHERE " + + s_parentNames[i] + "_ID=?) "; + List poList = new Query(Env.getCtx(), table.getName(), whereClause, trxName) + .setParameters(AD_Table_IDchild, Record_ID) + .list(); + + int count = 0; + for(PO po : poList) + { + po.deleteEx(true); + count++; + } + if(count > 0) { + if (log.isLoggable(Level.CONFIG)) log.config(table.getName() + " " + s_parentNames[i] + + " (" + AD_Table_ID + "/" + Record_ID + ") #" + count); } } } @@ -191,6 +155,38 @@ public class PO_Record } } + /** + * If a referencing Record ID or Record UU exists to the deleted record, set it to NULL + * @param AD_Table_ID + * @param Record_ID + * @param trxName + */ + public static void setRecordIdNull(int AD_Table_ID, int Record_ID, String trxName){ + KeyNamePair[] tables = getTablesWithRecordColumnFromCache(MColumn.FKCONSTRAINTTYPE_SetNull); + // Table loop + for (KeyNamePair table : tables) { + if(table.getKey() == AD_Table_ID) + continue; + + List poList = new Query(Env.getCtx(), table.getName(), " AD_Table_ID = ? AND Record_ID = ? ", trxName) + .setParameters(AD_Table_ID, Record_ID) + .list(); + + int count = 0; + for(PO po : poList) { + if(po.isColumnMandatory(po.get_ColumnIndex("Record_ID"))) + po.set_Value("Record_ID", 0); + else + po.set_Value("Record_ID", null); + po.saveEx(trxName); + count++; + } + if (count > 0) { + if (log.isLoggable(Level.CONFIG)) log.config(table.getName() + " (" + AD_Table_ID + "/" + Record_ID + ") #" + count); + } + } + } + /** * An entry Exists for restrict table/record combination * @param AD_Table_ID table @@ -200,16 +196,17 @@ public class PO_Record */ static String exists (int AD_Table_ID, int Record_ID, String trxName) { + KeyNamePair[] restricts = getTablesWithRecordColumnFromCache(MColumn.FKCONSTRAINTTYPE_NoAction); // Table Loop only - for (int i = 0; i < s_restricts.length; i++) + for (int i = 0; i < restricts.length; i++) { // SELECT COUNT(*) FROM table WHERE AD_Table_ID=#1 AND Record_ID=#2 StringBuilder sql = new StringBuilder ("SELECT COUNT(*) FROM ") - .append(s_restrictNames[i]) + .append(restricts[i].getName()) .append(" WHERE AD_Table_ID=? AND Record_ID=?"); int no = DB.getSQLValue(trxName, sql.toString(), AD_Table_ID, Record_ID); if (no > 0) - return s_restrictNames[i]; + return restricts[i].getName(); } return null; } // exists @@ -261,19 +258,77 @@ public class PO_Record */ static private void validate (int AD_Table_ID, String TableName) { - for (int i = 0; i < s_cascades.length; i++) + KeyNamePair[] cascades = getTablesWithRecordColumnFromCache(MColumn.FKCONSTRAINTTYPE_Cascade); + for (int i = 0; i < cascades.length; i++) { StringBuilder sql = new StringBuilder ("DELETE FROM ") - .append(s_cascadeNames[i]) + .append(cascades[i].getName()) .append(" WHERE AD_Table_ID=").append(AD_Table_ID) .append(" AND Record_ID NOT IN (SELECT ") .append(TableName).append("_ID FROM ").append(TableName).append(")"); int no = DB.executeUpdate(sql.toString(), null); if (no > 0) - if (log.isLoggable(Level.CONFIG)) log.config(s_cascadeNames[i] + " (" + AD_Table_ID + "/" + TableName + if (log.isLoggable(Level.CONFIG)) log.config(cascades[i].getName() + " (" + AD_Table_ID + "/" + TableName + ") Invalid #" + no); } } // validate + /** + * Get array of tables which has a Record_ID or Record_UU column with the defined Constraint Type from cache + * @param constraintType - FKConstraintType of AD_Column + * @return array of KeyNamePair + */ + static private KeyNamePair[] getTablesWithRecordColumnFromCache(String constraintType) { + KeyNamePair[] tables = s_po_record_tables_cache.get(constraintType); + if(tables != null) + return tables; + tables = getTablesWithRecordColumn(constraintType); + s_po_record_tables_cache.put(constraintType, tables); + return tables; + } + + /** + * Get array of tables which has a Record_ID or Record_UU column with the defined Constraint Type + * @param constraintType - FKConstraintType of AD_Column + * @return array of KeyNamePair + */ + static private KeyNamePair[] getTablesWithRecordColumn(String constraintType) { + ArrayList tables = new ArrayList(); + + String sql = " SELECT t.AD_Table_ID, TableName " + + " FROM AD_Column c " + + " JOIN AD_Table t ON (c.AD_Table_ID = t.AD_Table_ID) " + + " WHERE c.ColumnName IN (?,?) AND c.FKConstraintType = ? "; + PreparedStatement pstmt = null; + ResultSet rs = null; + try + { + pstmt = DB.prepareStatement (sql, null); + int idx = 1; + pstmt.setString(idx++, "Record_ID"); + pstmt.setString(idx++, "Record_UU"); + pstmt.setString(idx++, constraintType); + rs = pstmt.executeQuery (); + while (rs.next ()) + { + tables.add(new KeyNamePair(rs.getInt(1), rs.getString(2))); + } + } + catch (Exception e) + { + log.log (Level.SEVERE, sql, e); + } + finally { + DB.close(rs, pstmt); + rs = null; pstmt = null; + } + + KeyNamePair[] tablesArray = new KeyNamePair[tables.size()]; + for(int i=0; i