// Seal the model itself:
 static {
   COLLECTOR_MANAGEMENT_MODEL.seal();
 }
  @Override
  public void apply(SQLRecordStore<?, ?, ?> recordStore, UpgradeOperations upgradeOps)
      throws Exception {
    // Retrieve all known Models as RecordReferences:
    List<RecordReference> modelRecRefs =
        recordStore.retrieveRecordReferences(new RecordsQuery(Model.MODEL_SCHEMA));
    /* The RecordReferences contain enough information (all we really need is the modelID) and retrieving them does
     * *not* involve Model deserialisation (which would fail because the currently stored compressed serialised Java
     * objects are no longer compatible with the current Model, Schema, Column, etc. classes) */

    List<Record> newModelRecs = new ArrayList<Record>();
    List<Schema> schemata = new ArrayList<Schema>();

    // Get Model instances for the modelRecRefs and loop over them:
    for (Model model : getModels(recordStore, upgradeOps, modelRecRefs)) {
      // Create new Model record (to be stored in Models table below):
      newModelRecs.add(model.getModelRecord(client));

      // Remember schema for use below:
      schemata.addAll(model.getSchemata());
    }

    // Drop existing Models & Schemata tables (will be recreated below):
    upgradeOps.dropTable(recordStore, getOldTableName(Model.MODEL_SCHEMA), /*force:*/ true);
    upgradeOps.dropTable(recordStore, getOldTableName(Model.SCHEMA_SCHEMA), /*force:*/ true);

    // Create new Models table and insert new records:
    recordStore.store(
        newModelRecs); // this also achieves renaming the "compressedSerialisedObject" column to
                       // "serialisation"

    // List for the names of all tables that should be kept:
    Set<String> keepTables = new HashSet<String>(recordStore.getProtectedTableNames());

    // Loop over all schemata:
    for (Schema schema : schemata) {
      // Check if there is a table, with the old name (which is not necessarily different from the
      // new name), for the schema:
      if (!upgradeOps.doesTableExist(recordStore, getOldTableName(schema)))
        continue; // if there is no table we are done with this Schema

      // Remember (new) table so we don't delete the table below:
      keepTables.add(schema.tableName); // !!!

      //	Store new schemata (for new tablename) record:
      recordStore.store(
          schema.getMetaRecord()); // this also achieves adding new "flags" and "tableName" columns

      //	Rename table if necessary:
      String oldName = getOldTableName(schema);
      if (!oldName.equals(schema.tableName))
        upgradeOps.renameTable(recordStore, oldName, schema.tableName);

      // Get a TableConverter for the schema:
      TableConverter tableConverter =
          new TableConverter(
              schema,
              schema.flags
                  & ~StorageClient
                      .SCHEMA_FLAG_TRACK_LOSSLESSNESS); // un-set the lossless flag on the old
                                                        // schema

      // Add a column replacer to to deal with the added LosslessFlagColumn in schemas that have
      // that flag:
      if (schema.hasFlags(StorageClient.SCHEMA_FLAG_TRACK_LOSSLESSNESS))
        tableConverter.addColumnReplacer(
            new DefaultValueColumnAdder(
                LosslessFlagColumn.INSTANCE)); // (this makes the tableConverter non-transparent)

      // Check if the schema requires other conversions:
      boolean hasValueSetColWithAllOptionalSubCols = false;
      boolean hasListColumnThatNeedsConversion = false;
      boolean containsStringListColumns = false;
      for (Column<?> col : tableConverter.getOldSchema().getColumns(false)) {
        if (col instanceof ValueSetColumn<?, ?>
            && ((ValueSetColumn<?, ?>) col).hasAllOptionalSubColumns())
          hasValueSetColWithAllOptionalSubCols = true;
        else if (col instanceof ListColumn && !(col instanceof ByteArrayListColumn)) {
          hasListColumnThatNeedsConversion = true;
          if (((ListColumn<?, ?>) col).getSingleColumn() instanceof StringColumn)
            containsStringListColumns = true;
        }
      }

      /* Deal with changed number of bytes per character (4 instead of 3) in UTF-8 StringColumns
       * 	This is not relevant to StringColumns themselves because those remain backed by String-based SQLColumns.
       * 	Yet StringListColumn/ListColumn.Simple<String> are affected by the change as those used to be backed by
       * 	BLOB-bases SQLColumns. */
      if (containsStringListColumns)
        tableConverter.addColumnReplacer(
            new StringListColumnReplacer()); // (this makes the tableConverter non-transparent)

      // Give subclass the change to further tweak the table converter:
      customiseTableConverter(schema, tableConverter);

      // Check if we need to do anything:
      if (tableConverter.isTransparent()
          && !hasValueSetColWithAllOptionalSubCols
          && !hasListColumnThatNeedsConversion)
        // this schema/table does not need conversion.
        continue;

      if (hasValueSetColWithAllOptionalSubCols)
        // Temporarily disable the use of boolean columns to represent optional ValueSetColumns:
        upgradeOps.getTableFactory(recordStore).setInsertBoolColsForAllOptionalValueSetCols(false);
      /* This is required because the tables currently existing in the db are incompatible with
       * the SQLRecordStore#SQLTable instance we would get for the schema if we wouldn't disable
       * this behaviour. Disabling the behaviour ensures we get a SQLTable that is compatible with
       * the table as it exists in the db, enabling us to ... */

      if (hasListColumnThatNeedsConversion)
        // Temporarily switch to using BLOB-base SQLColumns for all ListColumns,
        //	so we can read from the existing BLOB-backed ListColumns:
        upgradeOps.getTableFactory(recordStore).setUseBLOBsForAllListColumns(true);

      // Make sure a new SQLTable instance will be constructed based on the old schema:
      upgradeOps.forgetTable(recordStore, schema.tableName);

      // Get all current records by querying the db with the oldSchema:
      List<Record> oldRecords = recordStore.retrieveRecords(tableConverter.getOldSchema());

      // Drop table (this will also get rid of the above-mentioned SQLTable instance):
      upgradeOps.dropTable(recordStore, schema.tableName, false);

      if (hasValueSetColWithAllOptionalSubCols)
        // Re-enable the use of boolean columns to represent optional ValueSetColumns:
        upgradeOps.getTableFactory(recordStore).setInsertBoolColsForAllOptionalValueSetCols(true);

      if (hasListColumnThatNeedsConversion)
        // Switch off use of BLOB-based SQLColumn for all ListColumns:
        upgradeOps.getTableFactory(recordStore).setUseBLOBsForAllListColumns(false);

      // Re-insert all converted records in new table (which will have the boolean column
      // representing the ValueSetColumn):
      recordStore.store(tableConverter.convertRecords(oldRecords));
    }

    // Delete unknown/unupgradable tables:
    for (String tableName : upgradeOps.getAllTableNames(recordStore))
      if (!keepTables.contains(tableName)) {
        upgradeOps.addWarning("Deleting unknown table \'" + tableName + "\'");
        upgradeOps.dropTable(recordStore, tableName, false);
      }

    // Upgrade step done!
  }