/*╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
 * database/ObjectStoreTyped.cpp is part of Brewtarget, and is copyright the following authors 2021-2026:
 *   • Matt Young <mfsy@yahoo.com>
 *
 * Brewtarget is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * Brewtarget is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌*/
#include "database/ObjectStoreTyped.h"

#include <array>
#include <mutex> // for std::once_flag

#include "database/DbTransaction.h"
#include "measurement/Unit.h"
#include "model/Boil.h"
#include "model/BoilStep.h"
#include "model/BrewLog.h"
#include "model/Equipment.h"
#include "model/Fermentable.h"
#include "model/Fermentation.h"
#include "model/FermentationStep.h"
#include "model/Folder.h"
#include "model/Hop.h"
#include "model/Instruction.h"
#include "model/StockPurchase.h"
#include "model/StockUseIngredient.h"
#include "model/StockPurchaseFermentable.h"
#include "model/StockPurchaseHop.h"
#include "model/StockPurchaseMisc.h"
#include "model/StockPurchaseSalt.h"
#include "model/StockPurchaseYeast.h"
#include "model/Mash.h"
#include "model/MashStep.h"
#include "model/Misc.h"
#include "model/RecipeAdditionFermentable.h"
#include "model/RecipeAdditionHop.h"
#include "model/RecipeAdditionMisc.h"
#include "model/RecipeAdditionYeast.h"
#include "model/RecipeAdjustmentSalt.h"
#include "model/RecipeUseOfWater.h"
#include "model/Recipe.h"
#include "model/Salt.h"
#include "model/Style.h"
#include "model/Water.h"
#include "model/Yeast.h"

namespace {
   //
   // By the magic of templated variables and template specialisation, we have below all the constructor parameters for
   // each type of ObjectStoreTyped.
   //
   // The only wrinkle here is that the order of definitions matters, eg the definition of PRIMARY_TABLE<BrewLog>
   // needs to appear after that of PRIMARY_TABLE<Recipe>, as the address of the latter is used in the former (to show
   // foreign key references).  However, as long as we don't want circular foreign key references in the database,
   // there should always be an order that works!
   //
   // We only use specialisations of these templates (PRIMARY_TABLE and JUNCTION_TABLES)
   //
   // It would be nice to be able to write here:
   //    template<class NE> ObjectStore::TableDefinition          const PRIMARY_TABLE   = delete;
   //    template<class NE> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES = delete;
   // However, this is not currently valid C++.  There was a proposal that would have made it so (see
   // https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2041r1.html) however, AFAICT this never made it into the
   // standard.
   //
   template<class NE> ObjectStore::TableDefinition          const PRIMARY_TABLE  {"", {}};
   template<class NE> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES{};

   //
   // NOTE: Unlike C++, SQL is generally case-insensitive, so we have slightly different naming conventions.
   //       Specifically, we use snake_case rather than camelCase for field and table names.  By convention, we also
   //       use upper case for SQL keywords and lower case for everything else.  This is in pursuit of making SQL
   //       slightly more readable.
   //

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Common Database field mappings for NamedEntity subclasses
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   std::vector<ObjectStore::TableField> const NAMED_ENTITY_COMMON_FIELDS {
      {
         // NB: We don't put the "id" (PropertyNames::NamedEntity::key) field here because it has to be the first one in
         //     each TableDefinition.  (The order of the other fields does not matter.)
         {ObjectStore::FieldType::String, {"name"   }, PropertyNames::NamedEntity::name   },
         {ObjectStore::FieldType::Bool  , {"deleted"}, PropertyNames::NamedEntity::deleted},
      }
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Folder<Recipe      >
   //                             Folder<Style       >
   //                             Folder<Equipment   >
   //                             Folder<Mash        >
   //                             Folder<Boil        >
   //                             Folder<Fermentation>
   //                             Folder<Fermentable >
   //                             Folder<Hop         >
   //                             Folder<Misc        >
   //                             Folder<Salt        >
   //                             Folder<Water       >
   //                             Folder<Yeast       >
   //
   // The tables are all structurally the same.  With the addition of a type column, they could be unified in one table,
   // but the advantage of keeping them separate is it helps enforce the design constraint that Hops can't go in a
   // Fermentable Folder etc.
   //
   // NB: `id` is the ID of the folder record; `contained_in_folder_id` is its "parent ID", ie the ID of the folder (if any) that
   //     contains this one.
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
#define TABLE_FOLDER_DEFN(BaseName, baseName) \
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Folder<BaseName>> { \
      "folder_for_" #baseName, \
      { \
         {ObjectStore::FieldType::Int, {"id"                    }, PropertyNames::NamedEntity::key}, \
         {ObjectStore::FieldType::Int, {"contained_in_folder_id"}, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<BaseName>>}, \
      }, \
      {&NAMED_ENTITY_COMMON_FIELDS} \
   }; \
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<Folder<BaseName>> {};

   TABLE_FOLDER_DEFN(Recipe      , recipe      )
   TABLE_FOLDER_DEFN(Style       , style       )
   TABLE_FOLDER_DEFN(Equipment   , equipment   )
   TABLE_FOLDER_DEFN(Mash        , mash        )
   TABLE_FOLDER_DEFN(Boil        , boil        )
   TABLE_FOLDER_DEFN(Fermentation, fermentation)
   TABLE_FOLDER_DEFN(Fermentable , fermentable )
   TABLE_FOLDER_DEFN(Hop         , hop         )
   TABLE_FOLDER_DEFN(Misc        , misc        )
   TABLE_FOLDER_DEFN(Salt        , salt        )
   TABLE_FOLDER_DEFN(Water       , water       )
   TABLE_FOLDER_DEFN(Yeast       , yeast       )

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Equipment
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Equipment> {
      "equipment",
      {
         {ObjectStore::FieldType::Int   , {"id"                            }, PropertyNames::NamedEntity::key                       },
         {ObjectStore::FieldType::Int   , {"contained_in_folder_id"        }, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<Equipment>>},
         {ObjectStore::FieldType::Double, {"fermenter_batch_size_l"        }, PropertyNames::Equipment::fermenterBatchSize_l        },
         {ObjectStore::FieldType::Double, {"boiling_point"                 }, PropertyNames::Equipment::boilingPoint_c              },
         {ObjectStore::FieldType::Double, {"kettle_boil_size_l"            }, PropertyNames::Equipment::kettleBoilSize_l            },
//         {ObjectStore::FieldType::Double, {"boil_time"                     }, PropertyNames::Equipment::boilTime_min                }, // Boil & BoilStep objects now supersede this
//         {ObjectStore::FieldType::Bool  , {"calc_boil_volume"              }, PropertyNames::Equipment::calcBoilVolume              },
         {ObjectStore::FieldType::Double, {"kettle_evaporation_per_hour_l" }, PropertyNames::Equipment::kettleEvaporationPerHour_l  },
         {ObjectStore::FieldType::Double, {"evap_rate"                     }, PropertyNames::Equipment::evapRate_pctHr              },
         {ObjectStore::FieldType::Double, {"mash_tun_grain_absorption_lkg" }, PropertyNames::Equipment::mashTunGrainAbsorption_LKg  },
         {ObjectStore::FieldType::Double, {"hop_utilization"               }, PropertyNames::Equipment::hopUtilization_pct          },
         {ObjectStore::FieldType::Double, {"lauter_tun_deadspace_loss_l"   }, PropertyNames::Equipment::lauterTunDeadspaceLoss_l    },
         {ObjectStore::FieldType::String, {"kettle_notes"                  }, PropertyNames::Equipment::kettleNotes                 },
         {ObjectStore::FieldType::Double, {"top_up_kettle"                 }, PropertyNames::Equipment::topUpKettle_l               },
         {ObjectStore::FieldType::Double, {"top_up_water"                  }, PropertyNames::Equipment::topUpWater_l                },
         {ObjectStore::FieldType::Double, {"kettle_trub_chiller_loss_l"    }, PropertyNames::Equipment::kettleTrubChillerLoss_l     },
         {ObjectStore::FieldType::Double, {"mash_tun_specific_heat_calgc"  }, PropertyNames::Equipment::mashTunSpecificHeat_calGC   },
         {ObjectStore::FieldType::Double, {"mash_tun_volume_l"             }, PropertyNames::Equipment::mashTunVolume_l             },
         {ObjectStore::FieldType::Double, {"mash_tun_weight_kg"            }, PropertyNames::Equipment::mashTunWeight_kg            },
         {ObjectStore::FieldType::Double, {"kettleInternalDiameter_cm"     }, PropertyNames::Equipment::kettleInternalDiameter_cm   },
         {ObjectStore::FieldType::Double, {"kettleOpeningDiameter_cm"      }, PropertyNames::Equipment::kettleOpeningDiameter_cm    },
         // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞
         {ObjectStore::FieldType::String, {"hlt_type"                      }, PropertyNames::Equipment::hltType                     },
         {ObjectStore::FieldType::String, {"mash_tun_type"                 }, PropertyNames::Equipment::mashTunType                 },
         {ObjectStore::FieldType::String, {"lauter_tun_type"               }, PropertyNames::Equipment::lauterTunType               },
         {ObjectStore::FieldType::String, {"kettle_type"                   }, PropertyNames::Equipment::kettleType                  },
         {ObjectStore::FieldType::String, {"fermenter_type"                }, PropertyNames::Equipment::fermenterType               },
         {ObjectStore::FieldType::String, {"agingvessel_type"              }, PropertyNames::Equipment::agingVesselType             },
         {ObjectStore::FieldType::String, {"packaging_vessel_type"         }, PropertyNames::Equipment::packagingVesselType         },
         {ObjectStore::FieldType::Double, {"hlt_volume_l"                  }, PropertyNames::Equipment::hltVolume_l                 },
         {ObjectStore::FieldType::Double, {"lauter_tun_volume_l"           }, PropertyNames::Equipment::lauterTunVolume_l           },
         {ObjectStore::FieldType::Double, {"aging_vessel_volume_l"         }, PropertyNames::Equipment::agingVesselVolume_l         },
         {ObjectStore::FieldType::Double, {"packaging_vessel_volume_l"     }, PropertyNames::Equipment::packagingVesselVolume_l     },
         {ObjectStore::FieldType::Double, {"hlt_loss_l"                    }, PropertyNames::Equipment::hltLoss_l                   },
         {ObjectStore::FieldType::Double, {"mash_tun_loss_l"               }, PropertyNames::Equipment::mashTunLoss_l               },
         {ObjectStore::FieldType::Double, {"fermenter_loss_l"              }, PropertyNames::Equipment::fermenterLoss_l             },
         {ObjectStore::FieldType::Double, {"aging_vessel_loss_l"           }, PropertyNames::Equipment::agingVesselLoss_l           },
         {ObjectStore::FieldType::Double, {"packaging_vessel_loss_l"       }, PropertyNames::Equipment::packagingVesselLoss_l       },
         {ObjectStore::FieldType::Double, {"kettle_outflow_per_minute_l"   }, PropertyNames::Equipment::kettleOutflowPerMinute_l    },
         {ObjectStore::FieldType::Double, {"hlt_weight_kg"                 }, PropertyNames::Equipment::hltWeight_kg                },
         {ObjectStore::FieldType::Double, {"lauter_tun_weight_kg"          }, PropertyNames::Equipment::lauterTunWeight_kg          },
         {ObjectStore::FieldType::Double, {"kettle_weight_kg"              }, PropertyNames::Equipment::kettleWeight_kg             },
         {ObjectStore::FieldType::Double, {"hlt_specific_heat_calgc"       }, PropertyNames::Equipment::hltSpecificHeat_calGC       },
         {ObjectStore::FieldType::Double, {"lauter_tun_specific_heat_calgc"}, PropertyNames::Equipment::lauterTunSpecificHeat_calGC },
         {ObjectStore::FieldType::Double, {"kettle_specific_heat_calgc"    }, PropertyNames::Equipment::kettleSpecificHeat_calGC    },
         {ObjectStore::FieldType::String, {"hlt_notes"                     }, PropertyNames::Equipment::hltNotes                    },
         {ObjectStore::FieldType::String, {"mash_tun_notes"                }, PropertyNames::Equipment::mashTunNotes                },
         {ObjectStore::FieldType::String, {"lauter_tun_notes"              }, PropertyNames::Equipment::lauterTunNotes              },
         {ObjectStore::FieldType::String, {"fermenter_notes"               }, PropertyNames::Equipment::fermenterNotes              },
         {ObjectStore::FieldType::String, {"aging_vessel_notes"            }, PropertyNames::Equipment::agingVesselNotes            },
         {ObjectStore::FieldType::String, {"packaging_vessel_notes"        }, PropertyNames::Equipment::packagingVesselNotes        },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Fermentable
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Fermentable> {
      "fermentable",
      {
         {ObjectStore::FieldType::Int   , {"id"                       }, PropertyNames::NamedEntity::key                       },
         {ObjectStore::FieldType::Int   , {"contained_in_folder_id"   }, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<Fermentable>>},
         {ObjectStore::FieldType::Double, {"coarse_fine_diff"         }, PropertyNames::Fermentable::coarseFineDiff_pct        },
         {ObjectStore::FieldType::Double, {"color_lovibond"           }, PropertyNames::Fermentable::color_lovibond            },
         {ObjectStore::FieldType::Double, {"diastatic_power"          }, PropertyNames::Fermentable::diastaticPower_lintner    },
         {ObjectStore::FieldType::Enum  , {"ftype"                    }, PropertyNames::Fermentable::type                      , &Fermentable::typeStringMapping},
         {ObjectStore::FieldType::Double, {"ibu_gal_per_lb"           }, PropertyNames::Fermentable::ibuGalPerLb               },
         {ObjectStore::FieldType::Double, {"max_in_batch"             }, PropertyNames::Fermentable::maxInBatch_pct            },
         {ObjectStore::FieldType::Double, {"moisture"                 }, PropertyNames::Fermentable::moisture_pct              },
         {ObjectStore::FieldType::String, {"notes"                    }, PropertyNames::Fermentable::notes                     },
         {ObjectStore::FieldType::String, {"origin"                   }, PropertyNames::Fermentable::origin                    },
         {ObjectStore::FieldType::String, {"supplier"                 }, PropertyNames::Fermentable::supplier                  },
         {ObjectStore::FieldType::Double, {"protein"                  }, PropertyNames::Fermentable::protein_pct               },
         {ObjectStore::FieldType::Bool  , {"recommend_mash"           }, PropertyNames::Fermentable::recommendMash             },
         // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞
         {ObjectStore::FieldType::Enum  , {"grain_group"              }, PropertyNames::Fermentable::grainGroup                , &Fermentable::grainGroupStringMapping},
         {ObjectStore::FieldType::String, {"producer"                 }, PropertyNames::Fermentable::producer                  },
         {ObjectStore::FieldType::String, {"product_id"               }, PropertyNames::Fermentable::productId                 },
         {ObjectStore::FieldType::Double, {"fine_grind_yield_pct"     }, PropertyNames::Fermentable::fineGrindYield_pct        }, // Replaces yield / yield_pct
         {ObjectStore::FieldType::Double, {"coarse_grind_yield_pct"   }, PropertyNames::Fermentable::coarseGrindYield_pct      },
         {ObjectStore::FieldType::Double, {"potential_yield_sg"       }, PropertyNames::Fermentable::potentialYield_sg         },
         {ObjectStore::FieldType::Double, {"alpha_amylase_dext_units" }, PropertyNames::Fermentable::alphaAmylase_dextUnits    },
         {ObjectStore::FieldType::Double, {"kolbach_index_pct"        }, PropertyNames::Fermentable::kolbachIndex_pct          },
         {ObjectStore::FieldType::Double, {"hardness_prp_glassy_pct"  }, PropertyNames::Fermentable::hardnessPrpGlassy_pct     },
         {ObjectStore::FieldType::Double, {"hardness_prp_half_pct"    }, PropertyNames::Fermentable::hardnessPrpHalf_pct       },
         {ObjectStore::FieldType::Double, {"hardness_prp_mealy_pct"   }, PropertyNames::Fermentable::hardnessPrpMealy_pct      },
         {ObjectStore::FieldType::Double, {"kernel_size_prp_plump_pct"}, PropertyNames::Fermentable::kernelSizePrpPlump_pct    },
         {ObjectStore::FieldType::Double, {"kernel_size_prp_thin_pct" }, PropertyNames::Fermentable::kernelSizePrpThin_pct     },
         {ObjectStore::FieldType::Double, {"friability_pct"           }, PropertyNames::Fermentable::friability_pct            },
         {ObjectStore::FieldType::Double, {"di_ph"                    }, PropertyNames::Fermentable::di_ph                     },
         {ObjectStore::FieldType::Double, {"viscosity_cp"             }, PropertyNames::Fermentable::viscosity_cP              },
         {ObjectStore::FieldType::Double, {"dmsp_ppm"                 }, PropertyNames::Fermentable::dmsP_ppm                  },
         {ObjectStore::FieldType::Double, {"fan_ppm"                  }, PropertyNames::Fermentable::fan_ppm                   },
         {ObjectStore::FieldType::Double, {"fermentability_pct"       }, PropertyNames::Fermentable::fermentability_pct        },
         {ObjectStore::FieldType::Double, {"beta_glucan_ppm"          }, PropertyNames::Fermentable::betaGlucan_ppm            },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Hop
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Hop> {
      "hop",
      {
         {ObjectStore::FieldType::Int   , {"id"                    }, PropertyNames::NamedEntity::key                       },
         {ObjectStore::FieldType::Int   , {"contained_in_folder_id"}, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<Hop>>},
         {ObjectStore::FieldType::Double, {"alpha"                 }, PropertyNames::Hop::alpha_pct                         },
         {ObjectStore::FieldType::Double, {"beta"                  }, PropertyNames::Hop::beta_pct                          },
         {ObjectStore::FieldType::Double, {"caryophyllene"         }, PropertyNames::Hop::caryophyllene_pct                 },
         {ObjectStore::FieldType::Double, {"cohumulone"            }, PropertyNames::Hop::cohumulone_pct                    },
         {ObjectStore::FieldType::Enum  , {"form"                  }, PropertyNames::Hop::form                              , &Hop::formStringMapping},
         {ObjectStore::FieldType::Double, {"hsi"                   }, PropertyNames::Hop::hsi_pct                           },
         {ObjectStore::FieldType::Double, {"humulene"              }, PropertyNames::Hop::humulene_pct                      },
         {ObjectStore::FieldType::Double, {"myrcene"               }, PropertyNames::Hop::myrcene_pct                       },
         {ObjectStore::FieldType::String, {"notes"                 }, PropertyNames::Hop::notes                             },
         {ObjectStore::FieldType::String, {"origin"                }, PropertyNames::Hop::origin                            },
         {ObjectStore::FieldType::String, {"substitutes"           }, PropertyNames::Hop::substitutes                       },
         {ObjectStore::FieldType::Enum  , {"htype"                 }, PropertyNames::Hop::type                              , &Hop::typeStringMapping},
         // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞
         {ObjectStore::FieldType::String, {"producer"              }, PropertyNames::Hop::producer                          },
         {ObjectStore::FieldType::String, {"product_id"            }, PropertyNames::Hop::productId                         },
         {ObjectStore::FieldType::String, {"year"                  }, PropertyNames::Hop::year                              },
         {ObjectStore::FieldType::Double, {"total_oil_ml_per_100g" }, PropertyNames::Hop::totalOil_mlPer100g                },
         {ObjectStore::FieldType::Double, {"farnesene_pct"         }, PropertyNames::Hop::farnesene_pct                     },
         {ObjectStore::FieldType::Double, {"geraniol_pct"          }, PropertyNames::Hop::geraniol_pct                      },
         {ObjectStore::FieldType::Double, {"b_pinene_pct"          }, PropertyNames::Hop::bPinene_pct                       },
         {ObjectStore::FieldType::Double, {"linalool_pct"          }, PropertyNames::Hop::linalool_pct                      },
         {ObjectStore::FieldType::Double, {"limonene_pct"          }, PropertyNames::Hop::limonene_pct                      },
         {ObjectStore::FieldType::Double, {"nerol_pct"             }, PropertyNames::Hop::nerol_pct                         },
         {ObjectStore::FieldType::Double, {"pinene_pct"            }, PropertyNames::Hop::pinene_pct                        },
         {ObjectStore::FieldType::Double, {"polyphenols_pct"       }, PropertyNames::Hop::polyphenols_pct                   },
         {ObjectStore::FieldType::Double, {"xanthohumol_pct"       }, PropertyNames::Hop::xanthohumol_pct                   },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Mash
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Mash> {
      "mash",
      {
         {ObjectStore::FieldType::Int   , {"id"                    }, PropertyNames::NamedEntity::key                       },
         {ObjectStore::FieldType::Int   , {"contained_in_folder_id"}, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<Mash>>},
         {ObjectStore::FieldType::Bool  , {"equip_adjust"          }, PropertyNames::Mash::equipAdjust                      },
         {ObjectStore::FieldType::Double, {"grain_temp"            }, PropertyNames::Mash::grainTemp_c                      },
         {ObjectStore::FieldType::String, {"notes"                 }, PropertyNames::Mash::notes                            },
         {ObjectStore::FieldType::Double, {"ph"                    }, PropertyNames::Mash::ph                               },
         {ObjectStore::FieldType::Double, {"sparge_temp"           }, PropertyNames::Mash::spargeTemp_c                     },
         {ObjectStore::FieldType::Double, {"tun_specific_heat"     }, PropertyNames::Mash::mashTunSpecificHeat_calGC        },
         {ObjectStore::FieldType::Double, {"tun_temp"              }, PropertyNames::Mash::tunTemp_c                        },
         {ObjectStore::FieldType::Double, {"tun_weight"            }, PropertyNames::Mash::mashTunWeight_kg                 },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };
   // Mashes don't have children, and the link with their MashSteps is stored in the MashStep (as between Recipe and BrewLogs)
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<Mash> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for MashStep
   // NB: MashSteps don't get folders, because they don't separate from their Mash
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<MashStep> {
      "mash_step",
      {
         {ObjectStore::FieldType::Int   , {"id"                       }, PropertyNames::   NamedEntity::key                },
         // NB: MashSteps don't have folders, as each one is owned by a Mash
         {ObjectStore::FieldType::Double, {"end_temp_c"               }, PropertyNames::       Step::endTemp_c             },
         {ObjectStore::FieldType::Double, {"infuse_temp_c"            }, PropertyNames::   MashStep::infuseTemp_c          },
         {ObjectStore::FieldType::Int   , {"mash_id"                  }, PropertyNames::EnumeratedBase::ownerId            , &PRIMARY_TABLE<Mash>},
         {ObjectStore::FieldType::Enum  , {"mstype"                   }, PropertyNames::   MashStep::type                  , &MashStep::typeStringMapping},
         {ObjectStore::FieldType::Double, {"ramp_time_mins"           }, PropertyNames::   StepBase::rampTime_mins         },
         {ObjectStore::FieldType::Int   , {"step_number"              }, PropertyNames::EnumeratedBase::sequenceNumber     },
         {ObjectStore::FieldType::Double, {"step_temp_c"              }, PropertyNames::   StepBase::startTemp_c           },
         {ObjectStore::FieldType::Double, {"step_time_mins"           }, PropertyNames::   StepBase::stepTime_mins         },
         // Now we support BeerJSON, amount_l unifies and replaces infuseAmount_l and decoctionAmount_l
         // See comment in model/MashStep.h for more info
         {ObjectStore::FieldType::Double, {"amount_l"                 }, PropertyNames::   MashStep::amount_l              },
         // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞
         {ObjectStore::FieldType::String, {"description"              }, PropertyNames::       Step::description           },
         {ObjectStore::FieldType::Double, {"liquor_to_grist_ratio_lkg"}, PropertyNames::   MashStep::liquorToGristRatio_lKg},
         {ObjectStore::FieldType::Double, {"start_acidity_ph"         }, PropertyNames::       Step::startAcidity_pH       },
         {ObjectStore::FieldType::Double, {"end_acidity_ph"           }, PropertyNames::       Step::endAcidity_pH         },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };
   // MashSteps don't have children
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<MashStep> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Boil
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Boil> {
      "boil",
      {
         {ObjectStore::FieldType::Int   , {"id"                    }, PropertyNames::NamedEntity::key                       },
         {ObjectStore::FieldType::Int   , {"contained_in_folder_id"}, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<Boil>>},
         {ObjectStore::FieldType::String, {"description"           }, PropertyNames::Boil::description                      },
         {ObjectStore::FieldType::String, {"notes"                 }, PropertyNames::Boil::notes                            },
         {ObjectStore::FieldType::Double, {"pre_boil_size_l"       }, PropertyNames::Boil::preBoilSize_l                    },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };
   // Boils don't have children, and the link with their BoilSteps is stored in the BoilStep (as between Recipe and BrewLogs)
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<Boil> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for BoilStep
   // NB: BoilSteps don't get folders, because they don't separate from their Boil
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<BoilStep> {
      "boil_step",
      {
         {ObjectStore::FieldType::Int   , {"id"              }, PropertyNames:: NamedEntity::key            },
         // NB: BoilSteps don't have folders, as each one is owned by a Boil
         {ObjectStore::FieldType::Double, {"step_time_mins"  }, PropertyNames::    StepBase::stepTime_mins  },
         {ObjectStore::FieldType::Double, {"start_temp_c"    }, PropertyNames::    StepBase::startTemp_c    },
         {ObjectStore::FieldType::Double, {"end_temp_c"      }, PropertyNames::        Step::endTemp_c      },
         {ObjectStore::FieldType::Double, {"ramp_time_mins"  }, PropertyNames::    StepBase::rampTime_mins  },
         {ObjectStore::FieldType::Int   , {"step_number"     }, PropertyNames:: EnumeratedBase::sequenceNumber},
         {ObjectStore::FieldType::Int   , {"boil_id"         }, PropertyNames:: EnumeratedBase::ownerId        , &PRIMARY_TABLE<Boil>},
         {ObjectStore::FieldType::String, {"description"     }, PropertyNames::        Step::description    },
         {ObjectStore::FieldType::Double, {"start_acidity_ph"}, PropertyNames::        Step::startAcidity_pH},
         {ObjectStore::FieldType::Double, {"end_acidity_ph"  }, PropertyNames::        Step::endAcidity_pH  },
         {ObjectStore::FieldType::Double, {"start_gravity_sg"}, PropertyNames::StepExtended::startGravity_sg},
         {ObjectStore::FieldType::Double, {"end_gravity_sg"  }, PropertyNames::StepExtended::  endGravity_sg},
         {ObjectStore::FieldType::Enum  , {"chilling_type"   }, PropertyNames::    BoilStep::chillingType   , &BoilStep::chillingTypeStringMapping},
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };
   // BoilSteps don't have children
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<BoilStep> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Fermentation
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Fermentation> {
      "fermentation",
      {
         {ObjectStore::FieldType::Int   , {"id"                    }, PropertyNames::NamedEntity::key                       },
         {ObjectStore::FieldType::Int   , {"contained_in_folder_id"}, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<Fermentation>>},
         {ObjectStore::FieldType::String, {"description"           }, PropertyNames::Fermentation::description              },
         {ObjectStore::FieldType::String, {"notes"                 }, PropertyNames::Fermentation::notes                    },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };
   // Fermentations don't have children, and the link with their FermentationSteps is stored in the FermentationStep (as between Recipe and BrewLogs)
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<Fermentation> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for FermentationStep
   //
   // NB: FermentationSteps don't get folders, because they don't separate from their Fermentation
   //
   // NB: Although FermentationStep inherits (via StepExtended) from Step, the rampTime_mins field is not used and
   //     should not be stored in the DB or serialised.  See comment in model/Step.h.
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<FermentationStep> {
      "fermentation_step",
      {
         {ObjectStore::FieldType::Int   , {"id"              }, PropertyNames::     NamedEntity::key            },
         // NB: FermentationSteps don't have folders, as each one is owned by a Fermentation
         {ObjectStore::FieldType::Double, {"step_time_mins"  }, PropertyNames::        StepBase::stepTime_mins  },
         {ObjectStore::FieldType::Double, {"start_temp_c"    }, PropertyNames::        StepBase::startTemp_c    },
         {ObjectStore::FieldType::Double, {"end_temp_c"      }, PropertyNames::            Step::endTemp_c      },
         {ObjectStore::FieldType::Int   , {"step_number"     }, PropertyNames::     EnumeratedBase::sequenceNumber},
         {ObjectStore::FieldType::Int   , {"fermentation_id" }, PropertyNames::     EnumeratedBase::ownerId        , &PRIMARY_TABLE<Fermentation>},
         {ObjectStore::FieldType::String, {"description"     }, PropertyNames::            Step::description    },
         {ObjectStore::FieldType::Double, {"start_acidity_ph"}, PropertyNames::            Step::startAcidity_pH},
         {ObjectStore::FieldType::Double, {"end_acidity_ph"  }, PropertyNames::            Step::  endAcidity_pH},
         {ObjectStore::FieldType::Double, {"start_gravity_sg"}, PropertyNames::    StepExtended::startGravity_sg},
         {ObjectStore::FieldType::Double, {"end_gravity_sg"  }, PropertyNames::    StepExtended::  endGravity_sg},
         {ObjectStore::FieldType::Bool  , {"free_rise"       }, PropertyNames::FermentationStep::freeRise       },
         {ObjectStore::FieldType::String, {"vessel"          }, PropertyNames::FermentationStep::vessel         },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };
   // FermentationSteps don't have children
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<FermentationStep> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Misc
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Misc> {
      "misc",
      {
         {ObjectStore::FieldType::Int   , {"id"                    }, PropertyNames::NamedEntity::key                       },
         {ObjectStore::FieldType::Int   , {"contained_in_folder_id"}, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<Misc>>},
         {ObjectStore::FieldType::Enum  , {"mtype"                 }, PropertyNames::Misc::type                             , &Misc::typeStringMapping},
         {ObjectStore::FieldType::String, {"use_for"               }, PropertyNames::Misc::useFor                           },
         {ObjectStore::FieldType::String, {"notes"                 }, PropertyNames::Misc::notes                            },
         // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞
         {ObjectStore::FieldType::String, {"producer"              }, PropertyNames::Misc::producer                         },
         {ObjectStore::FieldType::String, {"product_id"            }, PropertyNames::Misc::productId                        },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Salt
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Salt> {
      "salt",
      {
         {ObjectStore::FieldType::Int   , {"id"                    }, PropertyNames::NamedEntity::key                       },
         {ObjectStore::FieldType::Int   , {"contained_in_folder_id"}, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<Salt>>},
         {ObjectStore::FieldType::Double, {"percent_acid"          }, PropertyNames::Salt::percentAcid                      },
         {ObjectStore::FieldType::Enum  , {"stype"                 }, PropertyNames::Salt::type                             , &Salt::typeStringMapping},
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };
   // Salts don't have children
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<Salt> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Style
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Style> {
      "style",
      {
         {ObjectStore::FieldType::Int   , {"id"                    }, PropertyNames::NamedEntity::key                       },
         {ObjectStore::FieldType::Int   , {"contained_in_folder_id"}, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<Style>>},
         {ObjectStore::FieldType::Double, {"abv_max"               }, PropertyNames::Style::abvMax_pct                      },
         {ObjectStore::FieldType::Double, {"abv_min"               }, PropertyNames::Style::abvMin_pct                      },
         {ObjectStore::FieldType::Double, {"carb_max"              }, PropertyNames::Style::carbMax_vol                     },
         {ObjectStore::FieldType::Double, {"carb_min"              }, PropertyNames::Style::carbMin_vol                     },
         {ObjectStore::FieldType::String, {"category"              }, PropertyNames::Style::category                        },
         {ObjectStore::FieldType::String, {"category_number"       }, PropertyNames::Style::categoryNumber                  },
         {ObjectStore::FieldType::Double, {"color_max"             }, PropertyNames::Style::colorMax_srm                    },
         {ObjectStore::FieldType::Double, {"color_min"             }, PropertyNames::Style::colorMin_srm                    },
         {ObjectStore::FieldType::String, {"examples"              }, PropertyNames::Style::examples                        },
         {ObjectStore::FieldType::Double, {"fg_max"                }, PropertyNames::Style::fgMax                           },
         {ObjectStore::FieldType::Double, {"fg_min"                }, PropertyNames::Style::fgMin                           },
         {ObjectStore::FieldType::Double, {"ibu_max"               }, PropertyNames::Style::ibuMax                          },
         {ObjectStore::FieldType::Double, {"ibu_min"               }, PropertyNames::Style::ibuMin                          },
         {ObjectStore::FieldType::String, {"ingredients"           }, PropertyNames::Style::ingredients                     },
         {ObjectStore::FieldType::String, {"notes"                 }, PropertyNames::Style::notes                           },
         {ObjectStore::FieldType::Double, {"og_max"                }, PropertyNames::Style::ogMax                           },
         {ObjectStore::FieldType::Double, {"og_min"                }, PropertyNames::Style::ogMin                           },
         {ObjectStore::FieldType::String, {"style_guide"           }, PropertyNames::Style::styleGuide                      },
         {ObjectStore::FieldType::String, {"style_letter"          }, PropertyNames::Style::styleLetter                     },
         {ObjectStore::FieldType::Enum  , {"stype"                 }, PropertyNames::Style::type                            , &Style::typeStringMapping},
         // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞
         {ObjectStore::FieldType::String, {"aroma"                 }, PropertyNames::Style::aroma                           },
         {ObjectStore::FieldType::String, {"appearance"            }, PropertyNames::Style::appearance                      },
         {ObjectStore::FieldType::String, {"flavor"                }, PropertyNames::Style::flavor                          },
         {ObjectStore::FieldType::String, {"mouthfeel"             }, PropertyNames::Style::mouthfeel                       },
         {ObjectStore::FieldType::String, {"overall_impression"    }, PropertyNames::Style::overallImpression               },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Water
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Water> {
      "water",
      {
         {ObjectStore::FieldType::Int   , {"id"                    }, PropertyNames::NamedEntity::key                       },
         {ObjectStore::FieldType::Int   , {"contained_in_folder_id"}, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<Water>>},
         {ObjectStore::FieldType::String, {"notes"                 }, PropertyNames::Water::notes                           },
         {ObjectStore::FieldType::Double, {"calcium"               }, PropertyNames::Water::calcium_ppm                     },
         {ObjectStore::FieldType::Double, {"bicarbonate"           }, PropertyNames::Water::bicarbonate_ppm                 },
         {ObjectStore::FieldType::Double, {"sulfate"               }, PropertyNames::Water::sulfate_ppm                     },
         {ObjectStore::FieldType::Double, {"sodium"                }, PropertyNames::Water::sodium_ppm                      },
         {ObjectStore::FieldType::Double, {"chloride"              }, PropertyNames::Water::chloride_ppm                    },
         {ObjectStore::FieldType::Double, {"magnesium"             }, PropertyNames::Water::magnesium_ppm                   },
         {ObjectStore::FieldType::Double, {"ph"                    }, PropertyNames::Water::ph                              },
         {ObjectStore::FieldType::Double, {"alkalinity"            }, PropertyNames::Water::alkalinity_ppm                  },
         {ObjectStore::FieldType::Int   , {"wtype"                 }, PropertyNames::Water::type                            }, // TODO: Would be less fragile to store this as text than a number
         {ObjectStore::FieldType::Double, {"mash_ro"               }, PropertyNames::Water::mashRo_pct                      },
         {ObjectStore::FieldType::Double, {"sparge_ro"             }, PropertyNames::Water::spargeRo_pct                    },
         {ObjectStore::FieldType::Bool  , {"as_hco3"               }, PropertyNames::Water::alkalinityAsHCO3                },
         // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞
         {ObjectStore::FieldType::Double, {"carbonate_ppm"         }, PropertyNames::Water::carbonate_ppm                   },
         {ObjectStore::FieldType::Double, {"potassium_ppm"         }, PropertyNames::Water::potassium_ppm                   },
         {ObjectStore::FieldType::Double, {"iron_ppm"              }, PropertyNames::Water::iron_ppm                        },
         {ObjectStore::FieldType::Double, {"nitrate_ppm"           }, PropertyNames::Water::nitrate_ppm                     },
         {ObjectStore::FieldType::Double, {"nitrite_ppm"           }, PropertyNames::Water::nitrite_ppm                     },
         {ObjectStore::FieldType::Double, {"fluoride_ppm"          }, PropertyNames::Water::fluoride_ppm                    },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Yeast
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Yeast> {
      "yeast",
      {
         {ObjectStore::FieldType::Int   , {"id"                          }, PropertyNames::NamedEntity::key                       },
         {ObjectStore::FieldType::Int   , {"contained_in_folder_id"      }, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<Yeast>>},
         {ObjectStore::FieldType::Double, {"max_temperature"             }, PropertyNames::Yeast::maxTemperature_c                },
         {ObjectStore::FieldType::Double, {"min_temperature"             }, PropertyNames::Yeast::minTemperature_c                },
         {ObjectStore::FieldType::Enum  , {"flocculation"                }, PropertyNames::Yeast::flocculation                    , &Yeast::flocculationStringMapping},
         {ObjectStore::FieldType::Enum  , {"form"                        }, PropertyNames::Yeast::form                            , &Yeast::formStringMapping        },
         {ObjectStore::FieldType::Enum  , {"ytype"                       }, PropertyNames::Yeast::type                            , &Yeast::typeStringMapping        },
         {ObjectStore::FieldType::Int   , {"max_reuse"                   }, PropertyNames::Yeast::maxReuse                        },
         {ObjectStore::FieldType::String, {"best_for"                    }, PropertyNames::Yeast::bestFor                         },
         {ObjectStore::FieldType::String, {"laboratory"                  }, PropertyNames::Yeast::laboratory                      },
         {ObjectStore::FieldType::String, {"notes"                       }, PropertyNames::Yeast::notes                           },
         {ObjectStore::FieldType::String, {"product_id"                  }, PropertyNames::Yeast::productId                       }, // Manufacturer's product ID, so, unlike other blah_id fields, not a foreign key!
         // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞
         {ObjectStore::FieldType::Double, {"alcohol_tolerance_pct"       }, PropertyNames::Yeast::alcoholTolerance_pct            },
         {ObjectStore::FieldType::Double, {"attenuation_min_pct"         }, PropertyNames::Yeast::attenuationMin_pct              },
         {ObjectStore::FieldType::Double, {"attenuation_max_pct"         }, PropertyNames::Yeast::attenuationMax_pct              },
         {ObjectStore::FieldType::Bool  , {"phenolic_off_flavor_positive"}, PropertyNames::Yeast::phenolicOffFlavorPositive       },
         {ObjectStore::FieldType::Bool  , {"glucoamylase_positive"       }, PropertyNames::Yeast::glucoamylasePositive            },
         {ObjectStore::FieldType::Bool  , {"killer_producing_k1_toxin"   }, PropertyNames::Yeast::killerProducingK1Toxin          },
         {ObjectStore::FieldType::Bool  , {"killer_producing_k2_toxin"   }, PropertyNames::Yeast::killerProducingK2Toxin          },
         {ObjectStore::FieldType::Bool  , {"killer_producing_k28_toxin"  }, PropertyNames::Yeast::killerProducingK28Toxin         },
         {ObjectStore::FieldType::Bool  , {"killer_producing_klus_toxin" }, PropertyNames::Yeast::killerProducingKlusToxin        },
         {ObjectStore::FieldType::Bool  , {"killer_neutral"              }, PropertyNames::Yeast::killerNeutral                   },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Recipe
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Recipe> {
      "recipe",
      {
         {ObjectStore::FieldType::Int   , {"id"                      }, PropertyNames::NamedEntity::key                       },
         {ObjectStore::FieldType::Int   , {"contained_in_folder_id"  }, PropertyNames::FolderPropertyBase::containedInFolderId, &PRIMARY_TABLE<Folder<Recipe>>},
         {ObjectStore::FieldType::Double, {"age"                     }, PropertyNames::Recipe::age_days                       },
         {ObjectStore::FieldType::Double, {"age_temp"                }, PropertyNames::Recipe::ageTemp_c                      },
         {ObjectStore::FieldType::String, {"assistant_brewer"        }, PropertyNames::Recipe::asstBrewer                     },
         {ObjectStore::FieldType::Double, {"batch_size"              }, PropertyNames::Recipe::batchSize_l                    },
         {ObjectStore::FieldType::String, {"brewer"                  }, PropertyNames::Recipe::brewer                         },
         {ObjectStore::FieldType::Double, {"carb_volume"             }, PropertyNames::Recipe::carbonation_vols               },
         {ObjectStore::FieldType::Double, {"carbonationtemp_c"       }, PropertyNames::Recipe::carbonationTemp_c              },
         {ObjectStore::FieldType::Date  , {"date"                    }, PropertyNames::Recipe::date                           },
         {ObjectStore::FieldType::Double, {"efficiency"              }, PropertyNames::Recipe::efficiency_pct                 },
         {ObjectStore::FieldType::Int   , {"equipment_id"            }, PropertyNames::Recipe::equipmentId                    , &PRIMARY_TABLE<Equipment>},
         {ObjectStore::FieldType::Double, {"fg"                      }, PropertyNames::Recipe::fg                             },
         {ObjectStore::FieldType::Bool  , {"forced_carb"             }, PropertyNames::Recipe::forcedCarbonation              },
         {ObjectStore::FieldType::Double, {"keg_priming_factor"      }, PropertyNames::Recipe::kegPrimingFactor               },
         {ObjectStore::FieldType::Int   , {"mash_id"                 }, PropertyNames::Recipe::mashId                         , &PRIMARY_TABLE<Mash>},
         {ObjectStore::FieldType::String, {"notes"                   }, PropertyNames::Recipe::notes                          },
         {ObjectStore::FieldType::Double, {"og"                      }, PropertyNames::Recipe::og                             },
         {ObjectStore::FieldType::Double, {"priming_sugar_equiv"     }, PropertyNames::Recipe::primingSugarEquiv              },
         {ObjectStore::FieldType::String, {"priming_sugar_name"      }, PropertyNames::Recipe::primingSugarName               },
         {ObjectStore::FieldType::Int   , {"style_id"                }, PropertyNames::Recipe::styleId                        , &PRIMARY_TABLE<Style>},
         {ObjectStore::FieldType::String, {"taste_notes"             }, PropertyNames::Recipe::tasteNotes                     },
         {ObjectStore::FieldType::Double, {"taste_rating"            }, PropertyNames::Recipe::tasteRating                    },
         {ObjectStore::FieldType::Enum  , {"type"                    }, PropertyNames::Recipe::type                           , &Recipe::typeStringMapping},
         {ObjectStore::FieldType::Int   , {"ancestor_id"             }, PropertyNames::Recipe::ancestorId                     , &PRIMARY_TABLE<Recipe>},
         {ObjectStore::FieldType::Bool  , {"locked"                  }, PropertyNames::Recipe::locked                         },
         {ObjectStore::FieldType::Int   , {"boil_id"                 }, PropertyNames::Recipe::boilId                         , &PRIMARY_TABLE<Boil>},
         {ObjectStore::FieldType::Int   , {"fermentation_id"         }, PropertyNames::Recipe::fermentationId                 , &PRIMARY_TABLE<Fermentation>},
         // ⮜⮜⮜ All below added for BeerJSON support ⮞⮞⮞
         {ObjectStore::FieldType::Double, {"beer_acidity_ph"         }, PropertyNames::Recipe::beerAcidity_pH                 },
         {ObjectStore::FieldType::Double, {"apparent_attenuation_pct"}, PropertyNames::Recipe::apparentAttenuation_pct        },
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<Recipe> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Common Database field mappings for IngredientAmount subclasses
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   std::vector<ObjectStore::TableField> const INGREDIENT_AMOUNT_COMMON_FIELDS {
      {
         // NB: We don't put the PropertyNames::IngredientAmount::ingredientId field here because we want it to have a
         //     table-specific name.  Eg, in fermentable_in_recipe it is fermentable_id, where as in hop_in_recipe it
         //     is hop_id, etc
         {ObjectStore::FieldType::Double, {"quantity"}, PropertyNames::IngredientAmount::quantity},
         {ObjectStore::FieldType::Unit  , {"unit"    }, PropertyNames::IngredientAmount::unit    , &Measurement::Units::unitStringMapping},
      }
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Common Database field mappings for RecipeAddition subclasses
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   std::vector<ObjectStore::TableField> const RECIPE_ADDITION_COMMON_FIELDS {
      {
         // NB: We don't put the "id" field here because it has to be the first one in each TableDefinition
         // (The order of the other fields does not matter.)
         {ObjectStore::FieldType::Enum  , {"stage"            }, PropertyNames::RecipeAddition::stage           , &RecipeAddition::stageStringMapping},
         {ObjectStore::FieldType::Int   , {"step"             }, PropertyNames::RecipeAddition::step            },
         {ObjectStore::FieldType::Double, {"add_at_time_mins" }, PropertyNames::RecipeAddition::addAtTime_mins  },
         {ObjectStore::FieldType::Double, {"add_at_gravity_sg"}, PropertyNames::RecipeAddition::addAtGravity_sg },
         {ObjectStore::FieldType::Double, {"add_at_acidity_ph"}, PropertyNames::RecipeAddition::addAtAcidity_pH },
         {ObjectStore::FieldType::Double, {"duration_mins"    }, PropertyNames::RecipeAddition::duration_mins   },
      }
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for RecipeAdditionFermentable
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<RecipeAdditionFermentable> {
      "fermentable_in_recipe",
      {
         {ObjectStore::FieldType::Int   , {"id"            }, PropertyNames::NamedEntity::key                },
         {ObjectStore::FieldType::Int   , {"recipe_id"     }, PropertyNames::OwnedByRecipe::recipeId         , &PRIMARY_TABLE<Recipe>     },
         {ObjectStore::FieldType::Int   , {"fermentable_id"}, PropertyNames::IngredientAmount::ingredientId, &PRIMARY_TABLE<Fermentable>},
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &INGREDIENT_AMOUNT_COMMON_FIELDS, &RECIPE_ADDITION_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for RecipeAdditionHop
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<RecipeAdditionHop> {
      "hop_in_recipe",
      {
         {ObjectStore::FieldType::Int   , {"id"       }, PropertyNames::NamedEntity::key                },
         {ObjectStore::FieldType::Int   , {"recipe_id"}, PropertyNames::OwnedByRecipe::recipeId         , &PRIMARY_TABLE<Recipe>},
         {ObjectStore::FieldType::Int   , {"hop_id"   }, PropertyNames::IngredientAmount::ingredientId, &PRIMARY_TABLE<Hop>   },
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &INGREDIENT_AMOUNT_COMMON_FIELDS, &RECIPE_ADDITION_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for RecipeAdditionMisc
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<RecipeAdditionMisc> {
      "misc_in_recipe",
      {
         {ObjectStore::FieldType::Int   , {"id"       }, PropertyNames::NamedEntity::key                },
         {ObjectStore::FieldType::Int   , {"recipe_id"}, PropertyNames::OwnedByRecipe::recipeId         , &PRIMARY_TABLE<Recipe>},
         {ObjectStore::FieldType::Int   , {"misc_id"  }, PropertyNames::IngredientAmount::ingredientId, &PRIMARY_TABLE<Misc>   },
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &INGREDIENT_AMOUNT_COMMON_FIELDS, &RECIPE_ADDITION_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for RecipeAdditionYeast
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<RecipeAdditionYeast> {
      "yeast_in_recipe",
      {
         {ObjectStore::FieldType::Int   , {"id"                 }, PropertyNames::NamedEntity::key                      },
         {ObjectStore::FieldType::Int   , {"recipe_id"          }, PropertyNames::OwnedByRecipe::recipeId               , &PRIMARY_TABLE<Recipe>},
         {ObjectStore::FieldType::Int   , {"yeast_id"           }, PropertyNames::IngredientAmount::ingredientId      , &PRIMARY_TABLE<Yeast>   },
         {ObjectStore::FieldType::Double, {"attenuation_pct"    }, PropertyNames::RecipeAdditionYeast::attenuation_pct  },
         {ObjectStore::FieldType::Int   , {"times_cultured"     }, PropertyNames::RecipeAdditionYeast::timesCultured    },
         {ObjectStore::FieldType::Int   , {"cell_count_billions"}, PropertyNames::RecipeAdditionYeast::cellCountBillions},
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &INGREDIENT_AMOUNT_COMMON_FIELDS, &RECIPE_ADDITION_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for RecipeAdjustmentSalt
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<RecipeAdjustmentSalt> {
      "salt_in_recipe",
      {
         {ObjectStore::FieldType::Int   , {"id"         }, PropertyNames::NamedEntity::key                },
         {ObjectStore::FieldType::Int   , {"recipe_id"  }, PropertyNames::OwnedByRecipe::recipeId         , &PRIMARY_TABLE<Recipe>},
         {ObjectStore::FieldType::Int   , {"salt_id"    }, PropertyNames::IngredientAmount::ingredientId, &PRIMARY_TABLE<Salt>   },
         {ObjectStore::FieldType::Enum  , {"when_to_add"}, PropertyNames::RecipeAdjustmentSalt::whenToAdd , &RecipeAdjustmentSalt::whenToAddStringMapping},
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &INGREDIENT_AMOUNT_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for RecipeUseOfWater
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<RecipeUseOfWater> {
      "water_in_recipe",
      {
         {ObjectStore::FieldType::Int   , {"id"       }, PropertyNames::NamedEntity::key                },
         {ObjectStore::FieldType::Int   , {"recipe_id"}, PropertyNames::OwnedByRecipe::recipeId         , &PRIMARY_TABLE<Recipe>},
         {ObjectStore::FieldType::Int   , {"water_id" }, PropertyNames::IngredientAmount::ingredientId, &PRIMARY_TABLE<Water>   },
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &INGREDIENT_AMOUNT_COMMON_FIELDS}
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for BrewLog
   //
   // Note that the "name" of a BrewLog is shown as "batch number" in the UI.
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<BrewLog> {
      "brew_log",
      {
         {ObjectStore::FieldType::Int   , {"id"                     }, PropertyNames::NamedEntity::key          },
         // NB: BrewLogs don't have folders, as each one is owned by a Recipe
         {ObjectStore::FieldType::Double, {"abv"                    }, PropertyNames::BrewLog::abv              },
         {ObjectStore::FieldType::Double, {"attenuation"            }, PropertyNames::BrewLog::attenuation      },
         {ObjectStore::FieldType::Double, {"boil_off"               }, PropertyNames::BrewLog::boilOff_l        },
         {ObjectStore::FieldType::Date  , {"brewdate"               }, PropertyNames::BrewLog::brewDate         },
         {ObjectStore::FieldType::Double, {"brewhouse_eff"          }, PropertyNames::BrewLog::brewhouseEff_pct },
         {ObjectStore::FieldType::Double, {"eff_into_bk"            }, PropertyNames::BrewLog::effIntoBK_pct    },
         {ObjectStore::FieldType::Date  , {"fermentdate"            }, PropertyNames::BrewLog::fermentDate      },
         {ObjectStore::FieldType::Double, {"fg"                     }, PropertyNames::BrewLog::fg               },
         {ObjectStore::FieldType::Double, {"final_volume"           }, PropertyNames::BrewLog::finalVolume_l    },
         {ObjectStore::FieldType::Double, {"mash_final_temp"        }, PropertyNames::BrewLog::mashFinTemp_c    },
         {ObjectStore::FieldType::String, {"notes"                  }, PropertyNames::BrewLog::notes            },
         {ObjectStore::FieldType::Double, {"og"                     }, PropertyNames::BrewLog::og               },
         {ObjectStore::FieldType::Double, {"pitch_temp"             }, PropertyNames::BrewLog::pitchTemp_c      },
         {ObjectStore::FieldType::Double, {"post_boil_volume"       }, PropertyNames::BrewLog::postBoilVolume_l },
         {ObjectStore::FieldType::Double, {"projected_abv"          }, PropertyNames::BrewLog::projABV_pct      },
         {ObjectStore::FieldType::Double, {"projected_atten"        }, PropertyNames::BrewLog::projAtten        },
         {ObjectStore::FieldType::Double, {"projected_boil_grav"    }, PropertyNames::BrewLog::projBoilGrav     },
         {ObjectStore::FieldType::Double, {"projected_eff"          }, PropertyNames::BrewLog::projEff_pct      },
         {ObjectStore::FieldType::Double, {"projected_ferm_points"  }, PropertyNames::BrewLog::projFermPoints   },
         {ObjectStore::FieldType::Double, {"projected_fg"           }, PropertyNames::BrewLog::projFg           },
         {ObjectStore::FieldType::Double, {"projected_mash_fin_temp"}, PropertyNames::BrewLog::projMashFinTemp_c},
         {ObjectStore::FieldType::Double, {"projected_og"           }, PropertyNames::BrewLog::projOg           },
         {ObjectStore::FieldType::Double, {"projected_points"       }, PropertyNames::BrewLog::projPoints       },
         {ObjectStore::FieldType::Double, {"projected_strike_temp"  }, PropertyNames::BrewLog::projStrikeTemp_c },
         {ObjectStore::FieldType::Double, {"projected_vol_into_bk"  }, PropertyNames::BrewLog::projVolIntoBK_l  },
         {ObjectStore::FieldType::Double, {"projected_vol_into_ferm"}, PropertyNames::BrewLog::projVolIntoFerm_l},
         {ObjectStore::FieldType::Double, {"sg"                     }, PropertyNames::BrewLog::sg               },
         {ObjectStore::FieldType::Double, {"strike_temp"            }, PropertyNames::BrewLog::strikeTemp_c     },
         {ObjectStore::FieldType::Double, {"volume_into_bk"         }, PropertyNames::BrewLog::volumeIntoBK_l   },
         {ObjectStore::FieldType::Double, {"volume_into_fermenter"  }, PropertyNames::BrewLog::volumeIntoFerm_l },
         {ObjectStore::FieldType::Int   , {"recipe_id"              }, PropertyNames::OwnedByRecipe::recipeId    , &PRIMARY_TABLE<Recipe>},
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };
   // BrewLogs don't have children
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<BrewLog> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for Instruction
   // NB: instructions aren't displayed in trees, and get no folder
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<Instruction> {
      "instruction",
      {
         {ObjectStore::FieldType::Int   , {"id"           }, PropertyNames::NamedEntity::key       },
         {ObjectStore::FieldType::Int   , {"recipe_id"    }, PropertyNames::EnumeratedBase::ownerId   , &PRIMARY_TABLE<Recipe>},
         {ObjectStore::FieldType::Int   , {"step_number"  }, PropertyNames::EnumeratedBase::sequenceNumber},
         {ObjectStore::FieldType::String, {"directions"   }, PropertyNames::Instruction::directions   },
         {ObjectStore::FieldType::Bool  , {"has_timer"    }, PropertyNames::Instruction::hasTimer     },
         {ObjectStore::FieldType::String, {"timer_value"  }, PropertyNames::Instruction::timerValue   },
         {ObjectStore::FieldType::Bool  , {"completed"    }, PropertyNames::Instruction::completed    },
         {ObjectStore::FieldType::Double, {"interval_mins"}, PropertyNames::Instruction::interval_mins},
      },
      {&NAMED_ENTITY_COMMON_FIELDS}
   };
   // Instructions don't have children
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<Instruction> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Common Database field mappings for StockPurchaseFermentable, StockPurchaseHop, etc
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   std::vector<ObjectStore::TableField> const STOCK_PURCHASE_COMMON_FIELDS {
      {
         // NB: We don't put the "id" field here because it has to be the first one in each TableDefinition
         // (The order of the other fields does not matter.)

         // NB: The "quantity" column for the IngredientAmount::quantity (via INGREDIENT_AMOUNT_COMMON_FIELDS) is used
         //     to mean "quantity received".  For this optional StockPurchase::quantityOrdered field, we don't store
         //     units as they will be the same as IngredientAmount::unit.
         {ObjectStore::FieldType::Double, {"quantity_ordered"}, PropertyNames::StockPurchase::quantityOrdered},
         {ObjectStore::FieldType::Date  , {"date_received"   }, PropertyNames::StockPurchase::dateReceived   },
         {ObjectStore::FieldType::Date  , {"date_ordered"    }, PropertyNames::StockPurchase::dateOrdered    },
         {ObjectStore::FieldType::Date  , {"date_best_before"}, PropertyNames::StockPurchase::dateBestBefore },
         {ObjectStore::FieldType::String, {"supplier"        }, PropertyNames::StockPurchase::supplier       },
         {ObjectStore::FieldType::String, {"note"            }, PropertyNames::StockPurchase::note           },
         {ObjectStore::FieldType::Money , {"purchase_price_currency",
                                           "purchase_price"  }, PropertyNames::StockPurchase::purchasePrice  },
         // In theory we could try to be clever here and share the currency column between purchasePrice and
         // purchaseTax, but I think it would add a lot of complexity for very little gain.
         {ObjectStore::FieldType::Money , {"purchase_tax_currency",
                                           "purchase_tax"    }, PropertyNames::StockPurchase::purchaseTax    },
         {ObjectStore::FieldType::Money , {"shipping_cost_currency",
                                           "shipping_cost"   }, PropertyNames::StockPurchase::shippingCost   },
      }
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for StockPurchaseFermentable
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<StockPurchaseFermentable> {
      "fermentable_stock_purchase",
      {
         {ObjectStore::FieldType::Int, {"id"            }, PropertyNames::NamedEntity::key              },
         {ObjectStore::FieldType::Int, {"fermentable_id"}, PropertyNames::IngredientAmount::ingredientId, &PRIMARY_TABLE<Fermentable>},
         {ObjectStore::FieldType::Double, {"color_lovibond"}, PropertyNames::StockPurchaseFermentable::color_lovibond        },
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &INGREDIENT_AMOUNT_COMMON_FIELDS, &STOCK_PURCHASE_COMMON_FIELDS}
   };
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<StockPurchaseFermentable> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for StockPurchaseHop
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<StockPurchaseHop> {
      "hop_stock_purchase",
      {
         {ObjectStore::FieldType::Int   , {"id"       }, PropertyNames::NamedEntity::key              },
         {ObjectStore::FieldType::Int   , {"hop_id"   }, PropertyNames::IngredientAmount::ingredientId, &PRIMARY_TABLE<Hop>},
         {ObjectStore::FieldType::Double, {"alpha_pct"}, PropertyNames::StockPurchaseHop::alpha_pct       },
         // NB property is on StockPurchaseHop, but enum itself is defined in Hop
         {ObjectStore::FieldType::String, {"form"     }, PropertyNames::StockPurchaseHop::form            , &Hop::typeStringMapping},
         {ObjectStore::FieldType::String, {"year"     }, PropertyNames::StockPurchaseHop::year            },
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &INGREDIENT_AMOUNT_COMMON_FIELDS, &STOCK_PURCHASE_COMMON_FIELDS}
   };
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<StockPurchaseHop> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for StockPurchaseMisc
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<StockPurchaseMisc> {
      "misc_stock_purchase",
      {
         {ObjectStore::FieldType::Int, {"id"    }, PropertyNames::NamedEntity::key                     },
         {ObjectStore::FieldType::Int, {"misc_id"}, PropertyNames::IngredientAmount::ingredientId, &PRIMARY_TABLE<Misc>},
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &INGREDIENT_AMOUNT_COMMON_FIELDS, &STOCK_PURCHASE_COMMON_FIELDS}
   };
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<StockPurchaseMisc> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for StockPurchaseSalt
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<StockPurchaseSalt> {
      "salt_stock_purchase",
      {
         {ObjectStore::FieldType::Int, {"id"     }, PropertyNames::NamedEntity::key              },
         {ObjectStore::FieldType::Int, {"salt_id"}, PropertyNames::IngredientAmount::ingredientId, &PRIMARY_TABLE<Salt>},
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &INGREDIENT_AMOUNT_COMMON_FIELDS, &STOCK_PURCHASE_COMMON_FIELDS}
   };
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<StockPurchaseSalt> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for StockPurchaseYeast
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<StockPurchaseYeast> {
      "yeast_stock_purchase",
      {
         {ObjectStore::FieldType::Int, {"id"      }, PropertyNames::NamedEntity::key              },
         {ObjectStore::FieldType::Int, {"yeast_id"}, PropertyNames::IngredientAmount::ingredientId, &PRIMARY_TABLE<Yeast>},
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &INGREDIENT_AMOUNT_COMMON_FIELDS, &STOCK_PURCHASE_COMMON_FIELDS}
   };
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<StockPurchaseYeast> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Common Database field mappings for StockUseFermentable, StockUseHop, etc
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   std::vector<ObjectStore::TableField> const STOCK_USE_COMMON_FIELDS {
      {
         {ObjectStore::FieldType::Int   , {"sequence_number"}, PropertyNames::EnumeratedBase::sequenceNumber},
         {ObjectStore::FieldType::Date  , {"date"           }, PropertyNames::StockUse::date         },
         {ObjectStore::FieldType::Enum  , {"reason"         }, PropertyNames::StockUse::reason       , &StockUse::reasonStringMapping},
         {ObjectStore::FieldType::String, {"comment"        }, PropertyNames::StockUse::comment      },
         {ObjectStore::FieldType::Double, {"quantity_used"  }, PropertyNames::StockUse::quantityUsed },
         {ObjectStore::FieldType::Int   , {"brew_log_id"    }, PropertyNames::StockUse::brewLogId    , &PRIMARY_TABLE<BrewLog>},
      }
   };

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for StockUseFermentable
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<StockUseFermentable> {
      "fermentable_stock_use",
      {
         {ObjectStore::FieldType::Int, {"id"                     }, PropertyNames::NamedEntity::key       },
         {ObjectStore::FieldType::Int, {"fermentable_purchase_id"}, PropertyNames::EnumeratedBase::ownerId, &PRIMARY_TABLE<StockPurchaseFermentable>},
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &STOCK_USE_COMMON_FIELDS}
   };
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<StockUseFermentable> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for StockUseHop
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<StockUseHop> {
      "hop_stock_use",
      {
         {ObjectStore::FieldType::Int, {"id"             }, PropertyNames::NamedEntity::key       },
         {ObjectStore::FieldType::Int, {"hop_purchase_id"}, PropertyNames::EnumeratedBase::ownerId, &PRIMARY_TABLE<StockPurchaseHop>},
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &STOCK_USE_COMMON_FIELDS}
   };
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<StockUseHop> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for StockUseMisc
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<StockUseMisc> {
      "misc_stock_use",
      {
         {ObjectStore::FieldType::Int, {"id"         }, PropertyNames::NamedEntity::key       },
         {ObjectStore::FieldType::Int, {"misc_purchase_id"}, PropertyNames::EnumeratedBase::ownerId, &PRIMARY_TABLE<StockPurchaseMisc>},
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &STOCK_USE_COMMON_FIELDS}
   };
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<StockUseMisc> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for StockUseSalt
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<StockUseSalt> {
      "salt_stock_use",
      {
         {ObjectStore::FieldType::Int, {"id"         }, PropertyNames::NamedEntity::key       },
         {ObjectStore::FieldType::Int, {"salt_purchase_id"}, PropertyNames::EnumeratedBase::ownerId, &PRIMARY_TABLE<StockPurchaseSalt>},
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &STOCK_USE_COMMON_FIELDS}
   };
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<StockUseSalt> {};

   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   // Database field mappings for StockUseYeast
   //»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»
   template<> ObjectStore::TableDefinition const PRIMARY_TABLE<StockUseYeast> {
      "yeast_stock_use",
      {
         {ObjectStore::FieldType::Int, {"id"          }, PropertyNames::NamedEntity::key       },
         {ObjectStore::FieldType::Int, {"yeast_purchase_id"}, PropertyNames::EnumeratedBase::ownerId, &PRIMARY_TABLE<StockPurchaseYeast>},
      },
      {&NAMED_ENTITY_COMMON_FIELDS, &STOCK_USE_COMMON_FIELDS}
   };
   template<> ObjectStore::JunctionTableDefinitions const JUNCTION_TABLES<StockUseYeast> {};

}


template<class NE>
ObjectStoreTyped<NE> & ObjectStoreTyped<NE>::getInstance(Database * database) {
   //
   // As of C++11, simple "Meyers singleton" is now thread-safe -- see
   // https://www.modernescpp.com/index.php/thread-safe-initialization-of-a-singleton#h3-guarantees-of-the-c-runtime
   //
   static ObjectStoreTyped<NE> ostSingleton{NE::typeLookup, PRIMARY_TABLE<NE>, JUNCTION_TABLES<NE>};

   //
   // C++11 provides a thread-safe way to ensure singleton.loadAll() is called exactly once
   //
   // Note that, if caller has passed us a pointer to a Database object, it's because we have a new empty database and
   // are about to construct tables -- ie it's too soon to try to load data.
   //
   static std::once_flag initFlag;
   if (!database) {
      std::call_once(initFlag, &ObjectStoreTyped<NE>::loadAll, &ostSingleton, nullptr);
   }

   return ostSingleton;
}

namespace {
   //! Helper for \c postLoadInit
   template<typename T>
   concept HasConnectSignalsMemberFunction = requires(T t) {
      // Need `std::same_as<void>` rather than `void` here, otherwise get error that "return-type-requirement is not a
      // type-constraint".
      { t.connectSignals() } -> std::same_as<void>;
   };

   /**
    * Called from \c InitialiseAllObjectStores
    */
   template<class NE>
   void postLoadInit(ObjectStoreTyped<NE> const & store) {
      //
      // For the moment, we are assuming that it's only connecting signals that has to happen after all objects are
      // loaded.  So, if the class has a connectSignals() member function then we call it for each object of that class
      // in the store.
      //
      if constexpr (HasConnectSignalsMemberFunction<NE>) {
         qDebug() << Q_FUNC_INFO << "Connecting signals for" << store<< "objects";
         for (NE * ne : store.getAllRaw()) {
            ne->connectSignals();
         }
      }
      return;
   }
}


//
// We have to make sure that each version of the above function gets instantiated.  NOTE: This is the 1st of 4 places we
// need to add any new ObjectStoreTyped
//
// You might think the use in InitialiseAllObjectStores below is sufficient for this, but the GCC linker says
// otherwise.
//
template ObjectStoreTyped<Boil                     > & ObjectStoreTyped<Boil                     >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<BoilStep                 > & ObjectStoreTyped<BoilStep                 >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<BrewLog                  > & ObjectStoreTyped<BrewLog                  >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Equipment                > & ObjectStoreTyped<Equipment                >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Fermentable              > & ObjectStoreTyped<Fermentable              >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Fermentation             > & ObjectStoreTyped<Fermentation             >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<FermentationStep         > & ObjectStoreTyped<FermentationStep         >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Hop                      > & ObjectStoreTyped<Hop                      >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Instruction              > & ObjectStoreTyped<Instruction              >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Mash                     > & ObjectStoreTyped<Mash                     >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<MashStep                 > & ObjectStoreTyped<MashStep                 >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Misc                     > & ObjectStoreTyped<Misc                     >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Folder<Recipe      >   > & ObjectStoreTyped<Folder<Recipe      >   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Folder<Style       >   > & ObjectStoreTyped<Folder<Style       >   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Folder<Equipment   >   > & ObjectStoreTyped<Folder<Equipment   >   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Folder<Mash        >   > & ObjectStoreTyped<Folder<Mash        >   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Folder<Boil        >   > & ObjectStoreTyped<Folder<Boil        >   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Folder<Fermentation>   > & ObjectStoreTyped<Folder<Fermentation>   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Folder<Fermentable >   > & ObjectStoreTyped<Folder<Fermentable >   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Folder<Hop         >   > & ObjectStoreTyped<Folder<Hop         >   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Folder<Misc        >   > & ObjectStoreTyped<Folder<Misc        >   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Folder<Salt        >   > & ObjectStoreTyped<Folder<Salt        >   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Folder<Water       >   > & ObjectStoreTyped<Folder<Water       >   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Folder<Yeast       >   > & ObjectStoreTyped<Folder<Yeast       >   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Recipe                   > & ObjectStoreTyped<Recipe                   >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<RecipeAdditionFermentable> & ObjectStoreTyped<RecipeAdditionFermentable>::getInstance(Database * database = nullptr);
template ObjectStoreTyped<RecipeAdditionHop        > & ObjectStoreTyped<RecipeAdditionHop        >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<RecipeAdditionMisc       > & ObjectStoreTyped<RecipeAdditionMisc       >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<RecipeAdditionYeast      > & ObjectStoreTyped<RecipeAdditionYeast      >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<RecipeAdjustmentSalt     > & ObjectStoreTyped<RecipeAdjustmentSalt     >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<RecipeUseOfWater         > & ObjectStoreTyped<RecipeUseOfWater         >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Salt                     > & ObjectStoreTyped<Salt                     >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<StockPurchaseFermentable > & ObjectStoreTyped<StockPurchaseFermentable >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<StockPurchaseHop         > & ObjectStoreTyped<StockPurchaseHop         >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<StockPurchaseMisc        > & ObjectStoreTyped<StockPurchaseMisc        >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<StockPurchaseSalt        > & ObjectStoreTyped<StockPurchaseSalt        >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<StockPurchaseYeast       > & ObjectStoreTyped<StockPurchaseYeast       >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<StockUseFermentable      > & ObjectStoreTyped<StockUseFermentable      >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<StockUseHop              > & ObjectStoreTyped<StockUseHop              >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<StockUseMisc             > & ObjectStoreTyped<StockUseMisc             >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<StockUseSalt             > & ObjectStoreTyped<StockUseSalt             >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<StockUseYeast            > & ObjectStoreTyped<StockUseYeast            >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Style                    > & ObjectStoreTyped<Style                    >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Water                    > & ObjectStoreTyped<Water                    >::getInstance(Database * database = nullptr);
template ObjectStoreTyped<Yeast                    > & ObjectStoreTyped<Yeast                    >::getInstance(Database * database = nullptr);

bool InitialiseAllObjectStores(QString & errorMessage) {
   // It's deliberate that we don't stop after the first error.  If there is a problem, it's quite useful to know how
   // extensive it is.
   QStringList errors;
   // NOTE: This is the 2nd of 4 places we need to add any new ObjectStoreTyped
   if (ObjectStoreTyped<Boil                     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Boil"                     ; }
   if (ObjectStoreTyped<BoilStep                 >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "BoilStep"                 ; }
   if (ObjectStoreTyped<BrewLog                 >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "BrewLog"                 ; }
   if (ObjectStoreTyped<Equipment                >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Equipment"                ; }
   if (ObjectStoreTyped<Fermentable              >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Fermentable"              ; }
   if (ObjectStoreTyped<Fermentation             >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Fermentation"             ; }
   if (ObjectStoreTyped<FermentationStep         >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "FermentationStep"         ; }
   if (ObjectStoreTyped<Hop                      >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Hop"                      ; }
   if (ObjectStoreTyped<Instruction              >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Instruction"              ; }
   if (ObjectStoreTyped<Mash                     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Mash"                     ; }
   if (ObjectStoreTyped<MashStep                 >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "MashStep"                 ; }
   if (ObjectStoreTyped<Misc                     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Misc"                     ; }
   if (ObjectStoreTyped<Folder<Recipe      >     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Folder<Recipe      >"     ; }
   if (ObjectStoreTyped<Folder<Style       >     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Folder<Style       >"     ; }
   if (ObjectStoreTyped<Folder<Equipment   >     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Folder<Equipment   >"     ; }
   if (ObjectStoreTyped<Folder<Mash        >     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Folder<Mash        >"     ; }
   if (ObjectStoreTyped<Folder<Boil        >     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Folder<Boil        >"     ; }
   if (ObjectStoreTyped<Folder<Fermentation>     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Folder<Fermentation>"     ; }
   if (ObjectStoreTyped<Folder<Fermentable >     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Folder<Fermentable >"     ; }
   if (ObjectStoreTyped<Folder<Hop         >     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Folder<Hop         >"     ; }
   if (ObjectStoreTyped<Folder<Misc        >     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Folder<Misc        >"     ; }
   if (ObjectStoreTyped<Folder<Salt        >     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Folder<Salt        >"     ; }
   if (ObjectStoreTyped<Folder<Water       >     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Folder<Water       >"     ; }
   if (ObjectStoreTyped<Folder<Yeast       >     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Folder<Yeast       >"     ; }
   if (ObjectStoreTyped<Recipe                   >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Recipe"                   ; }
   if (ObjectStoreTyped<RecipeAdditionFermentable>::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "RecipeAdditionFermentable"; }
   if (ObjectStoreTyped<RecipeAdditionHop        >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "RecipeAdditionHop"        ; }
   if (ObjectStoreTyped<RecipeAdditionMisc       >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "RecipeAdditionMisc"       ; }
   if (ObjectStoreTyped<RecipeAdditionYeast      >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "RecipeAdditionYeast"      ; }
   if (ObjectStoreTyped<RecipeAdjustmentSalt     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "RecipeAdjustmentSalt"     ; }
   if (ObjectStoreTyped<RecipeUseOfWater         >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "RecipeUseOfWater"         ; }
   if (ObjectStoreTyped<Salt                     >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Salt"                     ; }
   if (ObjectStoreTyped<StockPurchaseFermentable >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "StockPurchaseFermentable" ; }
   if (ObjectStoreTyped<StockPurchaseHop         >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "StockPurchaseHop"         ; }
   if (ObjectStoreTyped<StockPurchaseMisc        >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "StockPurchaseMisc"        ; }
   if (ObjectStoreTyped<StockPurchaseSalt        >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "StockPurchaseSalt"        ; }
   if (ObjectStoreTyped<StockPurchaseYeast       >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "StockPurchaseYeast"       ; }
   if (ObjectStoreTyped<StockUseFermentable      >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "StockPurchaseFermentable" ; }
   if (ObjectStoreTyped<StockUseHop              >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "StockPurchaseHop"         ; }
   if (ObjectStoreTyped<StockUseMisc             >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "StockPurchaseMisc"        ; }
   if (ObjectStoreTyped<StockUseSalt             >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "StockPurchaseSalt"        ; }
   if (ObjectStoreTyped<StockUseYeast            >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "StockPurchaseYeast"       ; }
   if (ObjectStoreTyped<Style                    >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Style"                    ; }
   if (ObjectStoreTyped<Water                    >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Water"                    ; }
   if (ObjectStoreTyped<Yeast                    >::getInstance().state() == ObjectStore::State::ErrorInitialising) { errors << "Yeast"                    ; }

   if (errors.size() > 0) {
      qCritical() << Q_FUNC_INFO << "Errors loading" << errors.join(", ");
      errorMessage = QObject::tr("There were errors loading the following object store(s): %1").arg(errors.join(", "));
      return false;
   }

   //
   // Now that we have loaded all objects into memory, we have, in some cases, some post-construction initialisation to
   // do.  This is typically things such as connecting signals (eg between Recipes and their ingredients), which would
   // be hard to do in the constructors (because it would then create an order dependency in loading in objects from the
   // DB).
   //
   // We could omit from this list object stores that don't require any post-construction initialisation.  However, it's
   // simpler to just include everything here, and rely on the compiler to optimise out the cases where there is no work
   // to be done (see postLoadInit above).
   //
   // NOTE: This is the 3rd of 4 places we need to add any new ObjectStoreTyped
   postLoadInit(ObjectStoreTyped<Boil                      >::getInstance());
   postLoadInit(ObjectStoreTyped<BoilStep                  >::getInstance());
   postLoadInit(ObjectStoreTyped<BrewLog                  >::getInstance());
   postLoadInit(ObjectStoreTyped<Equipment                 >::getInstance());
   postLoadInit(ObjectStoreTyped<Fermentable               >::getInstance());
   postLoadInit(ObjectStoreTyped<Fermentation              >::getInstance());
   postLoadInit(ObjectStoreTyped<FermentationStep          >::getInstance());
   postLoadInit(ObjectStoreTyped<Folder<Recipe      >      >::getInstance());
   postLoadInit(ObjectStoreTyped<Folder<Style       >      >::getInstance());
   postLoadInit(ObjectStoreTyped<Folder<Equipment   >      >::getInstance());
   postLoadInit(ObjectStoreTyped<Folder<Mash        >      >::getInstance());
   postLoadInit(ObjectStoreTyped<Folder<Boil        >      >::getInstance());
   postLoadInit(ObjectStoreTyped<Folder<Fermentation>      >::getInstance());
   postLoadInit(ObjectStoreTyped<Folder<Fermentable >      >::getInstance());
   postLoadInit(ObjectStoreTyped<Folder<Hop         >      >::getInstance());
   postLoadInit(ObjectStoreTyped<Folder<Misc        >      >::getInstance());
   postLoadInit(ObjectStoreTyped<Folder<Salt        >      >::getInstance());
   postLoadInit(ObjectStoreTyped<Folder<Water       >      >::getInstance());
   postLoadInit(ObjectStoreTyped<Folder<Yeast       >      >::getInstance());
   postLoadInit(ObjectStoreTyped<Hop                       >::getInstance());
   postLoadInit(ObjectStoreTyped<Instruction               >::getInstance());
   postLoadInit(ObjectStoreTyped<StockUseFermentable       >::getInstance());
   postLoadInit(ObjectStoreTyped<StockUseHop               >::getInstance());
   postLoadInit(ObjectStoreTyped<StockUseMisc              >::getInstance());
   postLoadInit(ObjectStoreTyped<StockUseSalt              >::getInstance());
   postLoadInit(ObjectStoreTyped<StockUseYeast             >::getInstance());
   postLoadInit(ObjectStoreTyped<StockPurchaseFermentable  >::getInstance());
   postLoadInit(ObjectStoreTyped<StockPurchaseHop          >::getInstance());
   postLoadInit(ObjectStoreTyped<StockPurchaseMisc         >::getInstance());
   postLoadInit(ObjectStoreTyped<StockPurchaseSalt         >::getInstance());
   postLoadInit(ObjectStoreTyped<StockPurchaseYeast        >::getInstance());
   postLoadInit(ObjectStoreTyped<Mash                      >::getInstance());
   postLoadInit(ObjectStoreTyped<MashStep                  >::getInstance());
   postLoadInit(ObjectStoreTyped<Misc                      >::getInstance());
   postLoadInit(ObjectStoreTyped<Recipe                    >::getInstance());
   postLoadInit(ObjectStoreTyped<RecipeAdditionFermentable >::getInstance());
   postLoadInit(ObjectStoreTyped<RecipeAdditionHop         >::getInstance());
   postLoadInit(ObjectStoreTyped<RecipeAdditionMisc        >::getInstance());
   postLoadInit(ObjectStoreTyped<RecipeAdditionYeast       >::getInstance());
   postLoadInit(ObjectStoreTyped<RecipeAdjustmentSalt      >::getInstance());
   postLoadInit(ObjectStoreTyped<RecipeUseOfWater          >::getInstance());
   postLoadInit(ObjectStoreTyped<Salt                      >::getInstance());
   postLoadInit(ObjectStoreTyped<Style                     >::getInstance());
   postLoadInit(ObjectStoreTyped<Water                     >::getInstance());
   postLoadInit(ObjectStoreTyped<Yeast                     >::getInstance());

   return true;
}

namespace {
   QVector<ObjectStore const *> getAllObjectStores(Database * database = nullptr) {
      // NOTE: This is the 4th of 4 places we need to add any new ObjectStoreTyped
      static QVector<ObjectStore const *> allObjectStores {
         &ObjectStoreTyped<Boil                      >::getInstance(database),
         &ObjectStoreTyped<BoilStep                  >::getInstance(database),
         &ObjectStoreTyped<BrewLog                  >::getInstance(database),
         &ObjectStoreTyped<Equipment                 >::getInstance(database),
         &ObjectStoreTyped<Fermentable               >::getInstance(database),
         &ObjectStoreTyped<Fermentation              >::getInstance(database),
         &ObjectStoreTyped<FermentationStep          >::getInstance(database),
         &ObjectStoreTyped<Folder<Recipe      >      >::getInstance(database),
         &ObjectStoreTyped<Folder<Style       >      >::getInstance(database),
         &ObjectStoreTyped<Folder<Equipment   >      >::getInstance(database),
         &ObjectStoreTyped<Folder<Mash        >      >::getInstance(database),
         &ObjectStoreTyped<Folder<Boil        >      >::getInstance(database),
         &ObjectStoreTyped<Folder<Fermentation>      >::getInstance(database),
         &ObjectStoreTyped<Folder<Fermentable >      >::getInstance(database),
         &ObjectStoreTyped<Folder<Hop         >      >::getInstance(database),
         &ObjectStoreTyped<Folder<Misc        >      >::getInstance(database),
         &ObjectStoreTyped<Folder<Salt        >      >::getInstance(database),
         &ObjectStoreTyped<Folder<Water       >      >::getInstance(database),
         &ObjectStoreTyped<Folder<Yeast       >      >::getInstance(database),
         &ObjectStoreTyped<Hop                       >::getInstance(database),
         &ObjectStoreTyped<Instruction               >::getInstance(database),
         &ObjectStoreTyped<StockUseFermentable       >::getInstance(database),
         &ObjectStoreTyped<StockUseHop               >::getInstance(database),
         &ObjectStoreTyped<StockUseMisc              >::getInstance(database),
         &ObjectStoreTyped<StockUseSalt              >::getInstance(database),
         &ObjectStoreTyped<StockUseYeast             >::getInstance(database),
         &ObjectStoreTyped<StockPurchaseFermentable  >::getInstance(database),
         &ObjectStoreTyped<StockPurchaseHop          >::getInstance(database),
         &ObjectStoreTyped<StockPurchaseMisc         >::getInstance(database),
         &ObjectStoreTyped<StockPurchaseSalt         >::getInstance(database),
         &ObjectStoreTyped<StockPurchaseYeast        >::getInstance(database),
         &ObjectStoreTyped<Mash                      >::getInstance(database),
         &ObjectStoreTyped<MashStep                  >::getInstance(database),
         &ObjectStoreTyped<Misc                      >::getInstance(database),
         &ObjectStoreTyped<Recipe                    >::getInstance(database),
         &ObjectStoreTyped<RecipeAdditionFermentable >::getInstance(database),
         &ObjectStoreTyped<RecipeAdditionHop         >::getInstance(database),
         &ObjectStoreTyped<RecipeAdditionMisc        >::getInstance(database),
         &ObjectStoreTyped<RecipeAdditionYeast       >::getInstance(database),
         &ObjectStoreTyped<RecipeAdjustmentSalt      >::getInstance(database),
         &ObjectStoreTyped<RecipeUseOfWater          >::getInstance(database),
         &ObjectStoreTyped<Salt                      >::getInstance(database),
         &ObjectStoreTyped<Style                     >::getInstance(database),
         &ObjectStoreTyped<Water                     >::getInstance(database),
         &ObjectStoreTyped<Yeast                     >::getInstance(database),
      };
      return allObjectStores;
   }
}

bool CreateAllDatabaseTables(Database & database, QSqlDatabase & connection) {
   qDebug() << Q_FUNC_INFO;
   //
   // This is obviously deliberately two separate loops because we cannot add the constraints until after all the tables
   // have been created.
   //
   // We only need to pass the database parameter to getAllObjectStores() the first time it is called.
   //
   for (auto const store : getAllObjectStores(&database)) {
      qInfo() << Q_FUNC_INFO << "Creating tables for" << *store;
      if (!store->createTables(database, connection)) {
         return false;
      }
   }
   for (auto const store : getAllObjectStores()) {
      qInfo() << Q_FUNC_INFO << "Adding constraints for" << *store;
      if (!store->addTableConstraints(database, connection)) {
         return false;
      }
   }
   return true;
}

bool WriteAllObjectStoresToNewDb(Database & newDatabase, QSqlDatabase & connectionNew) {
   //
   // Start transaction
   // By the magic of RAII, this will abort if we exit this function (including by throwing an exception) without
   // having called dbTransaction.commit().  (It will also turn foreign keys back on either way -- whether the
   // transaction is committed or rolled back.)
   //
   DbTransaction dbTransaction{newDatabase, connectionNew, "Write All", DbTransaction::DISABLE_FOREIGN_KEYS};

   for (ObjectStore const * objectStore : getAllObjectStores()) {
      if (!objectStore->writeAllToNewDb(newDatabase, connectionNew)) {
         return false;
      }
   }

   dbTransaction.commit();
   return true;
}