docs: protected String checkAddDropWhitelist(final CatalogType suspect, final ChangeType changeType)
      {
          //Will catch several things that are actually just deployment changes, but don't care
          //to be more specific at this point
          m_canOccurWithElasticRebalance = false;
  
          // should generate this from spec.txt
          if (suspect instanceof User ||
              suspect instanceof Group ||
              suspect instanceof Procedure ||
              suspect instanceof Function ||
              suspect instanceof SnapshotSchedule ||
              // refs are safe to add drop if the thing they reference is
              suspect instanceof ConstraintRef ||
              suspect instanceof GroupRef ||
              suspect instanceof UserRef ||
              // The only meaty constraints (for now) are UNIQUE, PKEY and NOT NULL.
              // The UNIQUE and PKEY constraints are supported as index definitions.
              // NOT NULL is supported as a field on columns.
              // So, in short, all of these constraints will pass or fail tests of other catalog differences
              // Even if they did show up as Constraints in the catalog (for no apparent functional reason),
              // flagging their changes here would be redundant.
              suspect instanceof Constraint)
          {
              return null;
          }
          else if (suspect instanceof TimeToLive) {
              Column column = ((TimeToLive) suspect).getTtlcolumn();
              Table table = (Table) column.getParent();
              // view table can not have ttl columns
              if (m_inStrictMatViewDiffMode) {
                  return "May not dynamically add TTl on materialized view's columns.";
              }
              // stream table can not have ttl columns
              if (CatalogUtil.isTableExportOnly((Database)table.getParent(), table) ) {
                  return "May not dynamically add TTL on stream table's columns.";
              }
              return null;
          }
  
          else if (suspect instanceof Table) {
              Table tbl = (Table)suspect;
              if (TableType.isStream(tbl.getTabletype()) || TableType.isPersistentMigrate(tbl.getTabletype())) {
                  m_requiresNewExportGeneration = true;
              }
              // No special guard against dropping a table or view
              // (although some procedures may fail to plan)
              if (ChangeType.DELETION == changeType) {
                  return null;
              }
              String tableName = tbl.getTypeName();
  
              // Remember the name of the new table.
              m_newTables.add(tableName.toUpperCase());
  
              String viewName = null;
              String sourceTableName = null;
              // If this is a materialized view, and it's not safe for non-empty
              // tables, then we need to note this.  In this conditional, we set
              // viewName to nonNull if the view has unsafe operations.  Otherwise
              // we leave it as null.
              if (tbl.getMvhandlerinfo().size() > 0) {
                  MaterializedViewHandlerInfo mvhInfo = tbl.getMvhandlerinfo().get("mvhandlerinfo");
                  if ( mvhInfo != null ) {
                      if ( ! mvhInfo.getIssafewithnonemptysources()) {
                          // Set viewName, but don't set sourceTableName
                          // because this is a multi-table view.
                          viewName = tbl.getTypeName();
                      }
                  }
              } else if (tbl.getMaterializer() != null) {
                  MaterializedViewInfo mvInfo = MaterializedViewProcessor.getMaterializedViewInfo(tbl);
                  if (mvInfo != null && ( ! mvInfo.getIssafewithnonemptysources() ) ) {
                      // Set both names, as this is a single table view.
                      // We know getMaterializer() will return non-null.
                      viewName = tbl.getTypeName();
                      sourceTableName = tbl.getMaterializer().getTypeName();
                  }
              }
              // Skip guard for view on stream, given the fact that stream table is always empty
              if (viewName != null && !TableType.isStream(tbl.getMaterializer().getTabletype())) {
                  return createViewDisallowedMessage(viewName, sourceTableName);
              }
              // Otherwise, support add/drop of the top level object.
              return null;
          }
  
          else if (suspect instanceof Connector) {
              m_requiresNewExportGeneration = true;
              return null;
          }
  
          else if (suspect instanceof ConnectorTableInfo) {
              m_requiresNewExportGeneration = true;
              return null;
          }
  
          else if (suspect instanceof ConnectorProperty) {
              m_requiresNewExportGeneration = true;
              return null;
          }
  
          else if (suspect instanceof ColumnRef) {
              if (suspect.getParent() instanceof Index) {
                  Index parent = (Index) suspect.getParent();
  
                  if (parent.getUnique() && (changeType == ChangeType.DELETION)) {
                      CatalogMap<Index> newIndexes= m_newIndexesByTable.get(parent.getParent().getTypeName());
                      Index newIndex = newIndexes.get(parent.getTypeName());
  
                      if (!checkNewUniqueIndex(newIndex)) {
                          return "May not dynamically remove columns from unique index: " +
                                  parent.getTypeName();
                      }
                  }
              }
  
              // ColumnRef is not part of an index, index is not unique OR unique index is safe to create
              return null;
          }
  
          else if (suspect instanceof Column) {
              // Note: "return false;" vs. fall through, in any of these branches
              // overrides the grandfathering-in of added/dropped Column-typed
              // sub-components of Procedure, Connector, etc. as checked in the loop, below.
              // Is this safe/correct?
              Column column = (Column) suspect;
              Table table = (Table) column.getParent();
              if (m_inStrictMatViewDiffMode) {
                  return "May not dynamically add, drop, or rename materialized view columns.";
              }
              boolean isStreamOrStreamView = CatalogUtil.isTableExportOnly((Database)table.getParent(), table);
              if (isStreamOrStreamView) {
                  m_requiresNewExportGeneration = true;
              }
              if (changeType == ChangeType.ADDITION) {
                  Column col = (Column) suspect;
                  // Skip guard for view on stream, given the fact that stream table is always empty
                  if ((! col.getNullable()) && (col.getDefaultvalue() == null) && !isStreamOrStreamView) {
                      return "May not dynamically add non-nullable column without default value.";
                  }
              }
              // adding/dropping a column requires isolation from snapshots
              m_requiresSnapshotIsolation = true;
              return null;
          }
  
          // allow addition/deletion of indexes except for the addition
          // of certain unique indexes that might fail if created
          else if (suspect instanceof Index) {
              Index index = (Index) suspect;
  
              // it's cool to remove indexes
              if (changeType == ChangeType.DELETION) {
                  return null;
              }
  
              if (! index.getIssafewithnonemptysources()) {
                  return "Unable to create index " + index.getTypeName() +
                         " while the table contains data." +
                         " The index definition uses operations that cannot be applied " +
                         "if table " + index.getParent().getTypeName() + " is not empty.";
              }
  
              if (! index.getUnique()) {
                  return null;
              }
  
              // if adding a unique index, check if the columns in the new
              // index cover an existing index
              if (checkNewUniqueIndex(index)) {
                  return null;
              }
  
              // Note: return error vs. fall through, here
              // overrides the grandfathering-in of (any? possible?) added/dropped Index-typed
              // sub-components of Procedure, Connector, etc. as checked in the loop, below.
              return "May not dynamically add unique indexes that don't cover existing unique indexes.\n";
          }
  
          else if (suspect instanceof MaterializedViewInfo && ! m_inStrictMatViewDiffMode) {
              return null;
          }
  
          else if (isTableLimitDeleteStmt(suspect)) {
              return null;
          }
  
          //TODO: This code is also pretty fishy
          // -- See the "salmon of doubt" comment in checkModifyWhitelist
  
          // Also allow add/drop of anything (that hasn't triggered an early return already)
          // if it is found anywhere in these sub-trees.
          for (CatalogType parent = suspect.getParent(); parent != null; parent = parent.getParent()) {
              if (parent instanceof Procedure ||
                  parent instanceof Connector ||
                  parent instanceof ConstraintRef ||
                  parent instanceof Column) {
                  if (m_triggeredVerbosity) {
                      System.out.println("DEBUG VERBOSE diffRecursively " +
                                         ((changeType == ChangeType.ADDITION) ? "addition" : "deletion") +
                                         " of schema object '" + suspect + "'" +
                                         " rescued by context '" + parent + "'");
                  }
                  return null;
              }
          }
  
          return "May not dynamically add/drop schema object: '" + suspect + "'\n";
      }