docs: public String getDescriptionOfChanges(boolean updatedClass) {
          StringBuilder sb = new StringBuilder();
  
          sb.append("Catalog Difference Report\n");
          sb.append("=========================\n");
          boolean wroteChanges = false;
  
          // DESCRIBE TABLE CHANGES
          Namer tableNamer = new Namer() {
              @Override
              public String getName(CatalogType type) {
                  Table table = (Table) type;
                  // check if view
                  // note, this has to be pretty raw to avoid some smarts that wont work
                  // in this context. this may return an unresolved link which points nowhere,
                  // but that's good enough to know it's a view
                  if (table.getField("materializer") != null) {
                      return "View " + type.getTypeName();
                  }
  
                  // check if export table
                  // this probably doesn't work due to the same kinds of problems we have
                  // when identifying views. Tables just need a field that says if they
                  // are export tables or not... ugh. FIXME
                  for (Connector c : ((Database) table.getParent()).getConnectors()) {
                      for (ConnectorTableInfo cti : c.getTableinfo()) {
                          if (cti.getTable() == table) {
                              return "Stream Table " + type.getTypeName();
                          }
                      }
                  }
  
                  // just a regular table
                  return "Table " + type.getTypeName();
              }
          };
          wroteChanges |= basicMetaChangeDesc(sb, "TABLE CHANGES:", DiffClass.TABLE, null, tableNamer);
  
          // DESCRIBE PROCEDURE CHANGES
          Filter crudProcFilter = new Filter() {
              @Override
              public boolean include(CatalogType type) {
                  if (type.getTypeName().endsWith(".select")) return false;
                  if (type.getTypeName().endsWith(".insert")) return false;
                  if (type.getTypeName().endsWith(".delete")) return false;
                  if (type.getTypeName().endsWith(".update")) return false;
                  return true;
              }
          };
          wroteChanges |= basicMetaChangeDesc(sb, "PROCEDURE CHANGES:", DiffClass.PROC, crudProcFilter, null);
  
          // DESCRIBE FUNCTION CHANGES
          wroteChanges |= basicMetaChangeDesc(sb, "FUNCTION CHANGES:", DiffClass.FUNC, null, null);
  
          // DESCRIBE GROUP CHANGES
          wroteChanges |= basicMetaChangeDesc(sb, "GROUP CHANGES:", DiffClass.GROUP, null, null);
  
          // DESCRIBE USER CHANGES
          wroteChanges |= basicMetaChangeDesc(sb, "USER CHANGES:", DiffClass.USER, null, null);
  
          // DESCRIBE OTHER CHANGES
          CatalogChangeGroup group = m_changes.get(DiffClass.OTHER);
          if (group.groupChanges.size() > 0) {
              wroteChanges = true;
  
              sb.append("OTHER CHANGES:\n");
  
              assert(group.groupAdditions.size() == 0);
              assert(group.groupDeletions.size() == 0);
  
              for (TypeChanges metaChanges : group.groupChanges.values()) {
                  for (CatalogType type : metaChanges.typeAdditions) {
                      sb.append(String.format("  Catalog node %s of type %s has been added.\n",
                              type.getTypeName(), type.getClass().getSimpleName()));
                  }
                  for (CatalogType type : metaChanges.typeDeletions) {
                      sb.append(String.format("  Catalog node %s of type %s has been removed.\n",
                              type.getTypeName(), type.getClass().getSimpleName()));
                  }
                  for (FieldChange fc : metaChanges.childChanges.values()) {
                      sb.append(String.format("  Catalog node %s of type %s has modified metadata.\n",
                              fc.newType.getTypeName(), fc.newType.getClass().getSimpleName()));
                  }
              }
          }
  
          if (!wroteChanges) {
              if (updatedClass) {
                  sb.append("  Changes have been made to user code (procedures, supporting classes, etc).\n");
              } else {
                  sb.append("  No changes detected.\n");
              }
          }
  
          // trim the last newline
          sb.setLength(sb.length() - 1);
  
          return sb.toString();
      }