Merge "Add solver for april tag mapping"
diff --git a/WORKSPACE b/WORKSPACE
index 6ef1eb9..172e7bb 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1266,11 +1266,22 @@
     url = "https://www.frc971.org/Build-Dependencies/aws_sdk-19.0.0-RC1.tar.gz",
 )
 
+# Source code of LZ4 (files under lib/) are under BSD 2-Clause.
+# The rest of the repository (build information, documentation, etc.) is under GPLv2.
+# We only care about the lib/ subfolder anyways, and strip out any other files.
+http_archive(
+    name = "com_github_lz4_lz4",
+    build_file = "//debian:BUILD.lz4.bazel",
+    sha256 = "0b0e3aa07c8c063ddf40b082bdf7e37a1562bda40a0ff5272957f3e987e0e54b",
+    strip_prefix = "lz4-1.9.4/lib",
+    url = "https://github.com/lz4/lz4/archive/refs/tags/v1.9.4.tar.gz",
+)
+
 http_file(
     name = "com_github_foxglove_mcap_mcap",
     executable = True,
-    sha256 = "cf4dfcf71e20a60406aaded03a165312c1ca535b509ead90eb1846fc598137d2",
-    urls = ["https://github.com/foxglove/mcap/releases/download/releases%2Fmcap-cli%2Fv0.0.5/mcap-linux-amd64"],
+    sha256 = "ae745dd09cf4c9570c1c038a72630c07b073f0ed4b05983d64108ff748a40d3f",
+    urls = ["https://github.com/foxglove/mcap/releases/download/releases%2Fmcap-cli%2Fv0.0.22/mcap-linux-amd64"],
 )
 
 http_archive(
diff --git a/aos/BUILD b/aos/BUILD
index 596996b..164f40b 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -514,6 +514,7 @@
         "configuration_test.cc",
     ],
     data = [
+        "//aos/events:ping_fbs_reflection_out",
         "//aos/events:pingpong_config",
         "//aos/events:pong_fbs_reflection_out",
         "//aos/testdata:test_configs",
@@ -521,6 +522,7 @@
     target_compatible_with = ["@platforms//os:linux"],
     deps = [
         ":configuration",
+        "//aos/events:ping_fbs",
         "//aos/testing:flatbuffer_eq",
         "//aos/testing:googletest",
         "//aos/testing:path",
diff --git a/aos/configuration.cc b/aos/configuration.cc
index d5ac2a1..ad6fb54 100644
--- a/aos/configuration.cc
+++ b/aos/configuration.cc
@@ -68,7 +68,6 @@
 }
 
 }  // namespace
-
 // Define the compare and equal operators for Channel and Application so we can
 // insert them in the btree below.
 bool operator<(const FlatbufferDetachedBuffer<Channel> &lhs,
@@ -1594,5 +1593,30 @@
   return channel->num_readers() + channel->num_senders();
 }
 
+// Searches through configurations for schemas that include a certain type
+const reflection::Schema *GetSchema(const Configuration *config,
+                                    std::string_view schema_type) {
+  if (config->has_channels()) {
+    std::vector<flatbuffers::Offset<Channel>> channel_offsets;
+    for (const Channel *c : *config->channels()) {
+      if (schema_type == c->type()->string_view()) {
+        return c->schema();
+      }
+    }
+  }
+  return nullptr;
+}
+
+// Copy schema reflection into detached flatbuffer
+std::optional<FlatbufferDetachedBuffer<reflection::Schema>>
+GetSchemaDetachedBuffer(const Configuration *config,
+                        std::string_view schema_type) {
+  const reflection::Schema *found_schema = GetSchema(config, schema_type);
+  if (found_schema == nullptr) {
+    return std::nullopt;
+  }
+  return RecursiveCopyFlatBuffer(found_schema);
+}
+
 }  // namespace configuration
 }  // namespace aos
diff --git a/aos/configuration.h b/aos/configuration.h
index 8515b18..4d84b23 100644
--- a/aos/configuration.h
+++ b/aos/configuration.h
@@ -212,7 +212,20 @@
 // Returns the number of scratch buffers in the queue.
 int QueueScratchBufferSize(const Channel *channel);
 
-// TODO(austin): GetSchema<T>(const Flatbuffer<Configuration> &config);
+// Searches through configurations for schemas that include a certain type.
+const reflection::Schema *GetSchema(const Configuration *config,
+                                    std::string_view schema_type);
+
+// GetSchema template
+template <typename T>
+const reflection::Schema *GetSchema(const Configuration *config) {
+  return GetSchema(config, T::GetFullyQualifiedName());
+}
+
+// Copy schema reflection into detached flatbuffer
+std::optional<FlatbufferDetachedBuffer<reflection::Schema>>
+GetSchemaDetachedBuffer(const Configuration *config,
+                        std::string_view schema_type);
 
 }  // namespace configuration
 
@@ -222,6 +235,7 @@
                const FlatbufferDetachedBuffer<Channel> &rhs);
 bool operator==(const FlatbufferDetachedBuffer<Channel> &lhs,
                 const FlatbufferDetachedBuffer<Channel> &rhs);
+
 }  // namespace aos
 
 #endif  // AOS_CONFIGURATION_H_
diff --git a/aos/configuration_test.cc b/aos/configuration_test.cc
index fa74e20..fc45d88 100644
--- a/aos/configuration_test.cc
+++ b/aos/configuration_test.cc
@@ -1,6 +1,7 @@
 #include "aos/configuration.h"
 
 #include "absl/strings/strip.h"
+#include "aos/events/ping_generated.h"
 #include "aos/json_to_flatbuffer.h"
 #include "aos/testing/flatbuffer_eq.h"
 #include "aos/testing/path.h"
@@ -1007,10 +1008,47 @@
       JsonToFlatbuffer<Channel>(
           "{ \"name\": \"/foo\", \"type\": \".aos.bar\", \"num_readers\": 5, "
           "\"num_senders\": 10 }");
-
   EXPECT_EQ(QueueScratchBufferSize(&channel.message()), 15);
 }
 
+// Tests that GetSchema returns schema of specified type
+TEST_F(ConfigurationTest, GetSchema) {
+  FlatbufferDetachedBuffer<Configuration> config =
+      ReadConfig(ArtifactPath("aos/events/pingpong_config.json"));
+  FlatbufferVector<reflection::Schema> expected_schema =
+      FileToFlatbuffer<reflection::Schema>(
+          ArtifactPath("aos/events/ping.bfbs"));
+  EXPECT_EQ(FlatbufferToJson(GetSchema(&config.message(), "aos.examples.Ping")),
+            FlatbufferToJson(expected_schema));
+  EXPECT_EQ(GetSchema(&config.message(), "invalid_name"), nullptr);
+}
+
+// Tests that GetSchema template returns schema of specified type
+TEST_F(ConfigurationTest, GetSchemaTemplate) {
+  FlatbufferDetachedBuffer<Configuration> config =
+      ReadConfig(ArtifactPath("aos/events/pingpong_config.json"));
+  FlatbufferVector<reflection::Schema> expected_schema =
+      FileToFlatbuffer<reflection::Schema>(
+          ArtifactPath("aos/events/ping.bfbs"));
+  EXPECT_EQ(FlatbufferToJson(GetSchema<aos::examples::Ping>(&config.message())),
+            FlatbufferToJson(expected_schema));
+}
+
+// Tests that GetSchemaDetachedBuffer returns detached buffer of specified type
+TEST_F(ConfigurationTest, GetSchemaDetachedBuffer) {
+  FlatbufferDetachedBuffer<Configuration> config =
+      ReadConfig(ArtifactPath("aos/events/pingpong_config.json"));
+  FlatbufferVector<reflection::Schema> expected_schema =
+      FileToFlatbuffer<reflection::Schema>(
+          ArtifactPath("aos/events/ping.bfbs"));
+  EXPECT_EQ(FlatbufferToJson(
+                GetSchemaDetachedBuffer(&config.message(), "aos.examples.Ping")
+                    .value()),
+            FlatbufferToJson(expected_schema));
+  EXPECT_EQ(GetSchemaDetachedBuffer(&config.message(), "invalid_name"),
+            std::nullopt);
+}
+
 }  // namespace testing
 }  // namespace configuration
 }  // namespace aos
diff --git a/aos/events/event_loop_runtime.rs b/aos/events/event_loop_runtime.rs
index 360c931..023cfb6 100644
--- a/aos/events/event_loop_runtime.rs
+++ b/aos/events/event_loop_runtime.rs
@@ -370,6 +370,9 @@
         MonotonicInstant(self.0.monotonic_now())
     }
 
+    pub fn realtime_now(&self) -> RealtimeInstant {
+        RealtimeInstant(self.0.realtime_now())
+    }
     /// Note that the `'event_loop` input lifetime is intentional. The C++ API requires that it is
     /// part of `self.configuration()`, which will always have this lifetime.
     ///
@@ -711,7 +714,6 @@
 where
     T: Follow<'a> + 'a;
 
-// TODO(Brian): Add the realtime timestamps here.
 impl<'a, T> TypedContext<'a, T>
 where
     T: Follow<'a> + 'a,
@@ -730,6 +732,12 @@
     pub fn monotonic_remote_time(&self) -> MonotonicInstant {
         self.0.monotonic_remote_time()
     }
+    pub fn realtime_event_time(&self) -> RealtimeInstant {
+        self.0.realtime_event_time()
+    }
+    pub fn realtime_remote_time(&self) -> RealtimeInstant {
+        self.0.realtime_remote_time()
+    }
     pub fn queue_index(&self) -> u32 {
         self.0.queue_index()
     }
@@ -750,10 +758,11 @@
     T::Inner: fmt::Debug,
 {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        // TODO(Brian): Add the realtime timestamps here.
         f.debug_struct("TypedContext")
             .field("monotonic_event_time", &self.monotonic_event_time())
             .field("monotonic_remote_time", &self.monotonic_remote_time())
+            .field("realtime_event_time", &self.realtime_event_time())
+            .field("realtime_remote_time", &self.realtime_remote_time())
             .field("queue_index", &self.queue_index())
             .field("remote_queue_index", &self.remote_queue_index())
             .field("message", &self.message())
@@ -1020,10 +1029,11 @@
 
 impl fmt::Debug for Context<'_> {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        // TODO(Brian): Add the realtime timestamps here.
         f.debug_struct("Context")
             .field("monotonic_event_time", &self.monotonic_event_time())
             .field("monotonic_remote_time", &self.monotonic_remote_time())
+            .field("realtime_event_time", &self.realtime_event_time())
+            .field("realtime_remote_time", &self.realtime_remote_time())
             .field("queue_index", &self.queue_index())
             .field("remote_queue_index", &self.remote_queue_index())
             .field("size", &self.data().map(|data| data.len()))
@@ -1033,7 +1043,6 @@
     }
 }
 
-// TODO(Brian): Add the realtime timestamps here.
 impl<'context> Context<'context> {
     pub fn monotonic_event_time(self) -> MonotonicInstant {
         MonotonicInstant(self.0.monotonic_event_time)
@@ -1043,6 +1052,14 @@
         MonotonicInstant(self.0.monotonic_remote_time)
     }
 
+    pub fn realtime_event_time(self) -> RealtimeInstant {
+        RealtimeInstant(self.0.realtime_event_time)
+    }
+
+    pub fn realtime_remote_time(self) -> RealtimeInstant {
+        RealtimeInstant(self.0.realtime_remote_time)
+    }
+
     pub fn queue_index(self) -> u32 {
         self.0.queue_index
     }
@@ -1093,9 +1110,6 @@
 /// Represents a `aos::monotonic_clock::time_point` in a natural Rust way. This
 /// is intended to have the same API as [`std::time::Instant`], any missing
 /// functionality can be added if useful.
-///
-/// TODO(Brian): Do RealtimeInstant too. Use a macro? Integer as a generic
-/// parameter to distinguish them? Or just copy/paste?
 #[repr(transparent)]
 #[derive(Clone, Copy, Eq, PartialEq)]
 pub struct MonotonicInstant(i64);
@@ -1125,6 +1139,34 @@
     }
 }
 
+#[repr(transparent)]
+#[derive(Clone, Copy, Eq, PartialEq)]
+pub struct RealtimeInstant(i64);
+
+impl RealtimeInstant {
+    pub const MIN_TIME: Self = Self(i64::MIN);
+
+    pub fn is_min_time(self) -> bool {
+        self == Self::MIN_TIME
+    }
+
+    pub fn duration_since_epoch(self) -> Option<Duration> {
+        if self.is_min_time() {
+            None
+        } else {
+            Some(Duration::from_nanos(self.0.try_into().expect(
+                "monotonic_clock::time_point should always be after the epoch",
+            )))
+        }
+    }
+}
+
+impl fmt::Debug for RealtimeInstant {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.duration_since_epoch().fmt(f)
+    }
+}
+
 mod panic_waker {
     use std::task::{RawWaker, RawWakerVTable, Waker};
 
diff --git a/aos/util/BUILD b/aos/util/BUILD
index 19a9172..f8eb09f 100644
--- a/aos/util/BUILD
+++ b/aos/util/BUILD
@@ -86,6 +86,7 @@
         "//aos:fast_string_builder",
         "//aos:flatbuffer_utils",
         "//aos/events:event_loop",
+        "@com_github_lz4_lz4//:lz4",
         "@com_github_nlohmann_json//:json",
     ],
 )
diff --git a/aos/util/file.cc b/aos/util/file.cc
index 317206e..cdf0061 100644
--- a/aos/util/file.cc
+++ b/aos/util/file.cc
@@ -36,7 +36,6 @@
 void WriteStringToFileOrDie(const std::string_view filename,
                             const std::string_view contents,
                             mode_t permissions) {
-  ::std::string r;
   ScopedFD fd(open(::std::string(filename).c_str(),
                    O_CREAT | O_WRONLY | O_TRUNC, permissions));
   PCHECK(fd.get() != -1) << ": opening " << filename;
diff --git a/aos/util/log_to_mcap.cc b/aos/util/log_to_mcap.cc
index 5330c60..e669658 100644
--- a/aos/util/log_to_mcap.cc
+++ b/aos/util/log_to_mcap.cc
@@ -5,7 +5,12 @@
 
 DEFINE_string(node, "", "Node to replay from the perspective of.");
 DEFINE_string(output_path, "/tmp/log.mcap", "Log to output.");
-DEFINE_string(mode, "json", "json or flatbuffer serialization.");
+DEFINE_string(mode, "flatbuffer", "json or flatbuffer serialization.");
+DEFINE_bool(
+    canonical_channel_names, false,
+    "If set, use full channel names; by default, will shorten names to be the "
+    "shortest possible version of the name (e.g., /aos instead of /pi/aos).");
+DEFINE_bool(compress, true, "Whether to use LZ4 compression in MCAP file.");
 
 // Converts an AOS log to an MCAP log that can be fed into Foxglove. To try this
 // out, run:
@@ -19,21 +24,43 @@
 
   const std::vector<aos::logger::LogFile> logfiles =
       aos::logger::SortParts(aos::logger::FindLogs(argc, argv));
+  CHECK(!logfiles.empty());
+  const std::string logger_node = logfiles.at(0).logger_node;
+  bool all_logs_from_same_node = true;
+  for (const aos::logger::LogFile &log : logfiles) {
+    if (log.logger_node != logger_node) {
+      all_logs_from_same_node = false;
+      break;
+    }
+  }
+  std::string replay_node = FLAGS_node;
+  if (replay_node.empty() && all_logs_from_same_node) {
+    LOG(INFO) << "Guessing \"" << logger_node
+              << "\" as node given that --node was not specified.";
+    replay_node = logger_node;
+  }
 
   aos::logger::LogReader reader(logfiles);
 
   reader.Register();
 
   const aos::Node *node =
-      FLAGS_node.empty()
+      (replay_node.empty() ||
+       !aos::configuration::MultiNode(reader.configuration()))
           ? nullptr
-          : aos::configuration::GetNode(reader.configuration(), FLAGS_node);
+          : aos::configuration::GetNode(reader.configuration(), replay_node);
+
   std::unique_ptr<aos::EventLoop> mcap_event_loop =
       reader.event_loop_factory()->MakeEventLoop("mcap", node);
   CHECK(!FLAGS_output_path.empty());
-  aos::McapLogger relogger(mcap_event_loop.get(), FLAGS_output_path,
-                           FLAGS_mode == "flatbuffer"
-                               ? aos::McapLogger::Serialization::kFlatbuffer
-                               : aos::McapLogger::Serialization::kJson);
+  aos::McapLogger relogger(
+      mcap_event_loop.get(), FLAGS_output_path,
+      FLAGS_mode == "flatbuffer" ? aos::McapLogger::Serialization::kFlatbuffer
+                                 : aos::McapLogger::Serialization::kJson,
+      FLAGS_canonical_channel_names
+          ? aos::McapLogger::CanonicalChannelNames::kCanonical
+          : aos::McapLogger::CanonicalChannelNames::kShortened,
+      FLAGS_compress ? aos::McapLogger::Compression::kLz4
+                     : aos::McapLogger::Compression::kNone);
   reader.event_loop_factory()->Run();
 }
diff --git a/aos/util/log_to_mcap_test.py b/aos/util/log_to_mcap_test.py
index 7bdffe4..36f8de0 100644
--- a/aos/util/log_to_mcap_test.py
+++ b/aos/util/log_to_mcap_test.py
@@ -10,6 +10,27 @@
 from typing import Sequence, Text
 
 
+def make_permutations(options):
+    if len(options) == 0:
+        return [[]]
+    permutations = []
+    for option in options[0]:
+        for sub_permutations in make_permutations(options[1:]):
+            permutations.append([option] + sub_permutations)
+    return permutations
+
+
+def generate_argument_permutations():
+    arg_sets = [["--compress", "--nocompress"],
+                ["--mode=flatbuffer", "--mode=json"],
+                ["--canonical_channel_names", "--nocanonical_channel_names"],
+                ["--mcap_chunk_size=1000", "--mcap_chunk_size=10000000"],
+                ["--fetch", "--nofetch"]]
+    permutations = make_permutations(arg_sets)
+    print(permutations)
+    return permutations
+
+
 def main(argv: Sequence[Text]):
     parser = argparse.ArgumentParser()
     parser.add_argument("--log_to_mcap",
@@ -20,35 +41,38 @@
                         required=True,
                         help="Path to logfile generator.")
     args = parser.parse_args(argv)
-    with tempfile.TemporaryDirectory() as tmpdir:
-        log_name = tmpdir + "/test_log/"
-        mcap_name = tmpdir + "/log.mcap"
-        subprocess.run([args.generate_log, "--output_folder",
-                        log_name]).check_returncode()
-        # Run with a really small chunk size, to force a multi-chunk file.
-        subprocess.run([
-            args.log_to_mcap, "--output_path", mcap_name, "--mcap_chunk_size",
-            "1000", "--mode", "json", log_name
-        ]).check_returncode()
-        # MCAP attempts to find $HOME/.mcap.yaml, and dies on $HOME not existing. So
-        # give it an arbitrary config location (it seems to be fine with a non-existent config).
-        doctor_result = subprocess.run([
-            args.mcap, "doctor", mcap_name, "--config", tmpdir + "/.mcap.yaml"
-        ],
-                                       stdout=subprocess.PIPE,
-                                       stderr=subprocess.PIPE,
-                                       encoding='utf-8')
-        print(doctor_result.stdout)
-        print(doctor_result.stderr)
-        # mcap doctor doesn't actually return a non-zero exit code on certain failures...
-        # See https://github.com/foxglove/mcap/issues/356
-        if len(doctor_result.stderr) != 0:
-            print("Didn't expect any stderr output.")
-            return 1
-        if doctor_result.stdout != f"Examining {mcap_name}\n":
-            print("Only expected one line of stdout.")
-            return 1
-        doctor_result.check_returncode()
+    log_to_mcap_argument_permutations = generate_argument_permutations()
+    for log_to_mcap_args in log_to_mcap_argument_permutations:
+        with tempfile.TemporaryDirectory() as tmpdir:
+            log_name = tmpdir + "/test_log/"
+            mcap_name = tmpdir + "/log.mcap"
+            subprocess.run([args.generate_log, "--output_folder",
+                            log_name]).check_returncode()
+            # Run with a really small chunk size, to force a multi-chunk file.
+            subprocess.run([
+                args.log_to_mcap, "--output_path", mcap_name,
+                "--mcap_chunk_size", "1000", "--mode", "json", log_name
+            ] + log_to_mcap_args).check_returncode()
+            # MCAP attempts to find $HOME/.mcap.yaml, and dies on $HOME not existing. So
+            # give it an arbitrary config location (it seems to be fine with a non-existent config).
+            doctor_result = subprocess.run([
+                args.mcap, "doctor", mcap_name, "--config",
+                tmpdir + "/.mcap.yaml"
+            ],
+                                           stdout=subprocess.PIPE,
+                                           stderr=subprocess.PIPE,
+                                           encoding='utf-8')
+            print(doctor_result.stdout)
+            print(doctor_result.stderr)
+            # mcap doctor doesn't actually return a non-zero exit code on certain failures...
+            # See https://github.com/foxglove/mcap/issues/356
+            if len(doctor_result.stderr) != 0:
+                print("Didn't expect any stderr output.")
+                return 1
+            if doctor_result.stdout != f"Examining {mcap_name}\nHeader.profile field \"x-aos\" is not a well-known profile.\n":
+                print("Only expected two lines of stdout.")
+                return 1
+            doctor_result.check_returncode()
     return 0
 
 
diff --git a/aos/util/mcap_logger.cc b/aos/util/mcap_logger.cc
index dc27504..742f284 100644
--- a/aos/util/mcap_logger.cc
+++ b/aos/util/mcap_logger.cc
@@ -3,6 +3,8 @@
 #include "absl/strings/str_replace.h"
 #include "aos/configuration_schema.h"
 #include "aos/flatbuffer_merge.h"
+#include "lz4/lz4.h"
+#include "lz4/lz4frame.h"
 #include "single_include/nlohmann/json.hpp"
 
 DEFINE_uint64(mcap_chunk_size, 10'000'000,
@@ -82,11 +84,27 @@
   return schema;
 }
 
+namespace {
+std::string_view CompressionName(McapLogger::Compression compression) {
+  switch (compression) {
+    case McapLogger::Compression::kNone:
+      return "";
+    case McapLogger::Compression::kLz4:
+      return "lz4";
+  }
+  LOG(FATAL) << "Unreachable.";
+}
+}  // namespace
+
 McapLogger::McapLogger(EventLoop *event_loop, const std::string &output_path,
-                       Serialization serialization)
+                       Serialization serialization,
+                       CanonicalChannelNames canonical_channels,
+                       Compression compression)
     : event_loop_(event_loop),
       output_(output_path),
       serialization_(serialization),
+      canonical_channels_(canonical_channels),
+      compression_(compression),
       configuration_channel_([]() {
         // Setup a fake Channel for providing the configuration in the MCAP
         // file. This is included for convenience so that consumers of the MCAP
@@ -121,8 +139,10 @@
 
 McapLogger::~McapLogger() {
   // If we have any data remaining, write one last chunk.
-  if (current_chunk_.tellp() > 0) {
-    WriteChunk();
+  for (auto &pair : current_chunks_) {
+    if (pair.second.data.tellp() > 0) {
+      WriteChunk(&pair.second);
+    }
   }
   WriteDataEnd();
 
@@ -189,16 +209,18 @@
       message_counts_[id] = 0;
       event_loop_->MakeRawWatcher(
           channel, [this, id, channel](const Context &context, const void *) {
-            WriteMessage(id, channel, context, &current_chunk_);
-            if (static_cast<uint64_t>(current_chunk_.tellp()) >
+            ChunkStatus *chunk = &current_chunks_[id];
+            WriteMessage(id, channel, context, chunk);
+            if (static_cast<uint64_t>(chunk->data.tellp()) >
                 FLAGS_mcap_chunk_size) {
-              WriteChunk();
+              WriteChunk(chunk);
             }
           });
       fetchers_[id] = event_loop_->MakeRawFetcher(channel);
       event_loop_->OnRun([this, id, channel]() {
         if (FLAGS_fetch && fetchers_[id]->Fetch()) {
-          WriteMessage(id, channel, fetchers_[id]->context(), &current_chunk_);
+          WriteMessage(id, channel, fetchers_[id]->context(),
+                       &current_chunks_[id]);
         }
       });
     }
@@ -214,7 +236,7 @@
       config_context.size = configuration_.span().size();
       config_context.data = configuration_.span().data();
       WriteMessage(configuration_id_, &configuration_channel_.message(),
-                   config_context, &current_chunk_);
+                   config_context, &current_chunks_[configuration_id_]);
     });
   }
 
@@ -321,11 +343,34 @@
   // Schema ID
   AppendInt16(&string_builder_, schema_id);
   // Topic name
-  AppendString(&string_builder_,
-               override_name.empty()
-                   ? absl::StrCat(channel->name()->string_view(), " ",
-                                  channel->type()->string_view())
-                   : override_name);
+  std::string topic_name(override_name);
+  if (topic_name.empty()) {
+    switch (canonical_channels_) {
+      case CanonicalChannelNames::kCanonical:
+        topic_name = absl::StrCat(channel->name()->string_view(), " ",
+                                  channel->type()->string_view());
+        break;
+      case CanonicalChannelNames::kShortened: {
+        std::set<std::string> names = configuration::GetChannelAliases(
+            event_loop_->configuration(), channel, event_loop_->name(),
+            event_loop_->node());
+        std::string_view shortest_name;
+        for (const std::string &name : names) {
+          if (shortest_name.empty() || name.size() < shortest_name.size()) {
+            shortest_name = name;
+          }
+        }
+        if (shortest_name != channel->name()->string_view()) {
+          VLOG(1) << "Shortening " << channel->name()->string_view() << " "
+                  << channel->type()->string_view() << " to " << shortest_name;
+        }
+        topic_name =
+            absl::StrCat(shortest_name, " ", channel->type()->string_view());
+        break;
+      }
+    }
+  }
+  AppendString(&string_builder_, topic_name);
   // Encoding
   switch (serialization_) {
     case Serialization::kJson:
@@ -342,7 +387,7 @@
 }
 
 void McapLogger::WriteMessage(uint16_t channel_id, const Channel *channel,
-                              const Context &context, std::ostream *output) {
+                              const Context &context, ChunkStatus *chunk) {
   CHECK_NOTNULL(context.data);
 
   message_counts_[channel_id]++;
@@ -353,12 +398,13 @@
     earliest_message_ =
         std::min(context.monotonic_event_time, earliest_message_.value());
   }
-  if (!earliest_chunk_message_.has_value()) {
-    earliest_chunk_message_ = context.monotonic_event_time;
+  if (!chunk->earliest_message.has_value()) {
+    chunk->earliest_message = context.monotonic_event_time;
   } else {
-    earliest_chunk_message_ =
-        std::min(context.monotonic_event_time, earliest_chunk_message_.value());
+    chunk->earliest_message =
+        std::min(context.monotonic_event_time, chunk->earliest_message.value());
   }
+  chunk->latest_message = context.monotonic_event_time;
   latest_message_ = context.monotonic_event_time;
 
   string_builder_.Reset();
@@ -396,11 +442,12 @@
   total_message_bytes_ += context.size;
   total_channel_bytes_[channel] += context.size;
 
-  message_indices_[channel_id].push_back(std::make_pair<uint64_t, uint64_t>(
-      context.monotonic_event_time.time_since_epoch().count(),
-      output->tellp()));
+  chunk->message_indices[channel_id].push_back(
+      std::make_pair<uint64_t, uint64_t>(
+          context.monotonic_event_time.time_since_epoch().count(),
+          chunk->data.tellp()));
 
-  WriteRecord(OpCode::kMessage, string_builder_.Result(), output);
+  WriteRecord(OpCode::kMessage, string_builder_.Result(), &chunk->data);
 }
 
 void McapLogger::WriteRecord(OpCode op, std::string_view record,
@@ -412,43 +459,77 @@
   *ostream << record;
 }
 
-void McapLogger::WriteChunk() {
+void McapLogger::WriteChunk(ChunkStatus *chunk) {
   string_builder_.Reset();
 
-  CHECK(earliest_chunk_message_.has_value());
+  CHECK(chunk->earliest_message.has_value());
   const uint64_t chunk_offset = output_.tellp();
   AppendInt64(&string_builder_,
-              earliest_chunk_message_->time_since_epoch().count());
-  AppendInt64(&string_builder_, latest_message_.time_since_epoch().count());
+              chunk->earliest_message->time_since_epoch().count());
+  CHECK(chunk->latest_message.has_value());
+  AppendInt64(&string_builder_,
+              chunk->latest_message.value().time_since_epoch().count());
 
-  std::string chunk_records = current_chunk_.str();
+  std::string chunk_records = chunk->data.str();
   // Reset the chunk buffer.
-  current_chunk_.str("");
+  chunk->data.str("");
 
   const uint64_t records_size = chunk_records.size();
   // Uncompressed chunk size.
   AppendInt64(&string_builder_, records_size);
   // Uncompressed CRC (unpopulated).
   AppendInt32(&string_builder_, 0);
-  AppendString(&string_builder_, "");
-  AppendBytes(&string_builder_, chunk_records);
+  // Compression
+  AppendString(&string_builder_, CompressionName(compression_));
+  uint64_t records_size_compressed = records_size;
+  switch (compression_) {
+    case Compression::kNone:
+      AppendBytes(&string_builder_, chunk_records);
+      break;
+    case Compression::kLz4: {
+      // Default preferences.
+      LZ4F_preferences_t *lz4_preferences = nullptr;
+      const uint64_t max_size =
+          LZ4F_compressFrameBound(records_size, lz4_preferences);
+      CHECK_NE(0u, max_size);
+      if (max_size > compression_buffer_.size()) {
+        compression_buffer_.resize(max_size);
+      }
+      records_size_compressed = LZ4F_compressFrame(
+          compression_buffer_.data(), compression_buffer_.size(),
+          reinterpret_cast<const char *>(chunk_records.data()),
+          chunk_records.size(), lz4_preferences);
+      CHECK(!LZ4F_isError(records_size_compressed));
+      AppendBytes(&string_builder_,
+                  {reinterpret_cast<const char *>(compression_buffer_.data()),
+                   static_cast<size_t>(records_size_compressed)});
+      break;
+    }
+  }
   WriteRecord(OpCode::kChunk, string_builder_.Result());
 
   std::map<uint16_t, uint64_t> index_offsets;
   const uint64_t message_index_start = output_.tellp();
-  for (const auto &indices : message_indices_) {
+  for (const auto &indices : chunk->message_indices) {
     index_offsets[indices.first] = output_.tellp();
     string_builder_.Reset();
     AppendInt16(&string_builder_, indices.first);
     AppendMessageIndices(&string_builder_, indices.second);
     WriteRecord(OpCode::kMessageIndex, string_builder_.Result());
   }
-  message_indices_.clear();
+  chunk->message_indices.clear();
   chunk_indices_.push_back(ChunkIndex{
-      earliest_chunk_message_.value(), latest_message_, chunk_offset,
-      message_index_start - chunk_offset, records_size, index_offsets,
-      static_cast<uint64_t>(output_.tellp()) - message_index_start});
-  earliest_chunk_message_.reset();
+      .start_time = chunk->earliest_message.value(),
+      .end_time = chunk->latest_message.value(),
+      .offset = chunk_offset,
+      .chunk_size = message_index_start - chunk_offset,
+      .records_size = records_size,
+      .records_size_compressed = records_size_compressed,
+      .message_index_offsets = index_offsets,
+      .message_index_size =
+          static_cast<uint64_t>(output_.tellp()) - message_index_start,
+      .compression = compression_});
+  chunk->earliest_message.reset();
 }
 
 McapLogger::SummaryOffset McapLogger::WriteStatistics() {
@@ -491,9 +572,9 @@
     AppendChannelMap(&string_builder_, index.message_index_offsets);
     AppendInt64(&string_builder_, index.message_index_size);
     // Compression used.
-    AppendString(&string_builder_, "");
+    AppendString(&string_builder_, CompressionName(index.compression));
     // Compressed and uncompressed records size.
-    AppendInt64(&string_builder_, index.records_size);
+    AppendInt64(&string_builder_, index.records_size_compressed);
     AppendInt64(&string_builder_, index.records_size);
     WriteRecord(OpCode::kChunkIndex, string_builder_.Result());
   }
diff --git a/aos/util/mcap_logger.h b/aos/util/mcap_logger.h
index d7409fb..70a1328 100644
--- a/aos/util/mcap_logger.h
+++ b/aos/util/mcap_logger.h
@@ -36,8 +36,24 @@
     kJson,
     kFlatbuffer,
   };
+  // Whether to attempt to shorten channel names.
+  enum class CanonicalChannelNames {
+    // Just use the full, unambiguous, channel names.
+    kCanonical,
+    // Use GetChannelAliases() to determine the shortest possible name for the
+    // channel for the current node, and use that in the MCAP file. This makes
+    // it so that the channels in the resulting file are more likely to match
+    // the channel names that are used in "real" applications.
+    kShortened,
+  };
+  // Chunk compression to use in the MCAP file.
+  enum class Compression {
+    kNone,
+    kLz4,
+  };
   McapLogger(EventLoop *event_loop, const std::string &output_path,
-             Serialization serialization);
+             Serialization serialization,
+             CanonicalChannelNames canonical_channels, Compression compression);
   ~McapLogger();
 
  private:
@@ -77,8 +93,10 @@
     uint64_t offset;
     // Total size of the Chunk, in bytes.
     uint64_t chunk_size;
-    // Total size of the records portion of the Chunk, in bytes.
+    // Total uncompressed size of the records portion of the Chunk, in bytes.
     uint64_t records_size;
+    // Total size of the records portion of the Chunk, when compressed
+    uint64_t records_size_compressed;
     // Mapping of channel IDs to the MessageIndex entry for that channel within
     // the referenced Chunk. The MessageIndex is referenced by an offset from
     // the start of the file.
@@ -86,6 +104,30 @@
     // Total size, in bytes, of all the MessageIndex entries for this Chunk
     // together (note that they are required to be contiguous).
     uint64_t message_index_size;
+    // Compression used in this Chunk.
+    Compression compression;
+  };
+  // Maintains the state of a single Chunk. In order to maximize read
+  // performance, we currently maintain separate chunks for each channel so
+  // that, in order to read a given channel, only data associated with that
+  // channel nead be read.
+  struct ChunkStatus {
+    // Buffer containing serialized message data for the currently-being-built
+    // chunk.
+    std::stringstream data;
+    // Earliest message observed in this chunk.
+    std::optional<aos::monotonic_clock::time_point> earliest_message;
+    // Latest message observed in this chunk.
+    std::optional<aos::monotonic_clock::time_point> latest_message;
+    // MessageIndex's for each message. The std::map is indexed by channel ID.
+    // The vector is then a series of pairs of (timestamp, offset from start of
+    // data).
+    // Note that currently this will only ever have one entry, for the channel
+    // that this chunk corresponds to. However, the standard provides for there
+    // being more than one channel per chunk and so we still have some code that
+    // supports that.
+    std::map<uint16_t, std::vector<std::pair<uint64_t, uint64_t>>>
+        message_indices;
   };
   enum class RegisterHandlers { kYes, kNo };
   // Helpers to write each type of relevant record.
@@ -98,8 +140,8 @@
                     const aos::Channel *channel,
                     std::string_view override_name = "");
   void WriteMessage(uint16_t channel_id, const Channel *channel,
-                    const Context &context, std::ostream *output);
-  void WriteChunk();
+                    const Context &context, ChunkStatus *chunk);
+  void WriteChunk(ChunkStatus *chunk);
 
   // The helpers for writing records which appear in the Summary section will
   // return SummaryOffset's so that they can be referenced in the SummaryOffset
@@ -131,28 +173,23 @@
   aos::EventLoop *event_loop_;
   std::ofstream output_;
   const Serialization serialization_;
+  const CanonicalChannelNames canonical_channels_;
+  const Compression compression_;
   size_t total_message_bytes_ = 0;
   std::map<const Channel *, size_t> total_channel_bytes_;
-  // Buffer containing serialized message data for the currently-being-built
-  // chunk.
-  std::stringstream current_chunk_;
   FastStringBuilder string_builder_;
 
   // Earliest message observed in this logfile.
   std::optional<aos::monotonic_clock::time_point> earliest_message_;
-  // Earliest message observed in the current chunk.
-  std::optional<aos::monotonic_clock::time_point> earliest_chunk_message_;
-  // Latest message observed.
+  // Latest message observed in this logfile.
   aos::monotonic_clock::time_point latest_message_ =
       aos::monotonic_clock::min_time;
   // Count of all messages on each channel, indexed by channel ID.
   std::map<uint16_t, uint64_t> message_counts_;
   std::map<uint16_t, std::unique_ptr<RawFetcher>> fetchers_;
-  // MessageIndex's for each message. The std::map is indexed by channel ID. The
-  // vector is then a series of pairs of (timestamp, offset from start of
-  // current_chunk_).
-  std::map<uint16_t, std::vector<std::pair<uint64_t, uint64_t>>>
-      message_indices_;
+  // All currently-being-built chunks. Indexed by channel ID. This is used to
+  // segregate channels into separate chunks to support more efficient reading.
+  std::map<uint16_t, ChunkStatus> current_chunks_;
   // ChunkIndex's for all fully written Chunks.
   std::vector<ChunkIndex> chunk_indices_;
 
@@ -162,6 +199,9 @@
   uint16_t configuration_id_ = 0;
   FlatbufferDetachedBuffer<Channel> configuration_channel_;
   FlatbufferDetachedBuffer<Configuration> configuration_;
+
+  // Memory buffer to use for compressing data.
+  std::vector<uint8_t> compression_buffer_;
 };
 }  // namespace aos
 #endif  // AOS_UTIL_MCAP_LOGGER_H_
diff --git a/debian/BUILD.lz4.bazel b/debian/BUILD.lz4.bazel
new file mode 100644
index 0000000..7aa8def
--- /dev/null
+++ b/debian/BUILD.lz4.bazel
@@ -0,0 +1,22 @@
+licenses(["notice"])
+
+cc_library(
+    name = "lz4",
+    srcs = [
+        "lz4.c",
+        "lz4frame.c",
+        "lz4hc.c",
+        "xxhash.c",
+    ],
+    hdrs = [
+        # lz4hc.c tries to #include lz4.c....
+        "lz4.c",
+        "lz4.h",
+        "lz4frame.h",
+        "lz4hc.h",
+        "xxhash.h",
+    ],
+    include_prefix = "lz4",
+    includes = ["."],
+    visibility = ["//visibility:public"],
+)
diff --git a/frc971/control_loops/python/path_edit.py b/frc971/control_loops/python/path_edit.py
index 7c742c8..437b242 100755
--- a/frc971/control_loops/python/path_edit.py
+++ b/frc971/control_loops/python/path_edit.py
@@ -48,7 +48,10 @@
         # init editing / viewing modes and pointer location
         self.mode = Mode.kPlacing
         self.mousex = 0
+        self.lastx = 0
         self.mousey = 0
+        self.lasty = 0
+        self.drag_start = None
         self.module_path = os.path.dirname(os.path.realpath(sys.argv[0]))
         self.path_to_export = os.path.join(self.module_path,
                                            'points_for_pathedit.json')
@@ -406,6 +409,8 @@
             self.queue_draw()
 
     def do_button_release_event(self, event):
+        self.drag_start = None
+
         self.attempt_append_multisplines()
         self.mousex, self.mousey = self.input_transform.transform_point(
             event.x, event.y)
@@ -430,6 +435,9 @@
         self.mousex, self.mousey = self.input_transform.transform_point(
             event.x, event.y)
 
+        self.lastx = event.x
+        self.lasty = event.y
+
         if self.mode == Mode.kPlacing:
             if self.active_multispline.addPoint(self.mousex, self.mousey):
                 self.mode = Mode.kEditing
@@ -462,6 +470,9 @@
                                 index_multisplines, index_splines,
                                 index_points)
 
+                if self.control_point_index == None:
+                    self.drag_start = (event.x, event.y)
+
             multispline, result = Multispline.nearest_distance(
                 self.multisplines, cur_p)
             if result and result.fun < 0.1:
@@ -484,6 +495,14 @@
 
             multispline.update_lib_spline()
             self.graph.schedule_recalculate(self.multisplines)
+
+        if self.mode == Mode.kEditing and self.drag_start != None and self.control_point_index == None:
+
+            self.zoom_transform.translate(event.x - self.lastx,
+                                          event.y - self.lasty)
+            self.lastx = event.x
+            self.lasty = event.y
+
         self.queue_draw()
 
     def do_scroll_event(self, event):
@@ -505,9 +524,9 @@
         scale = (self.field.width + scale_by) / self.field.width
 
         # This restricts the amount it can be scaled.
-        if self.zoom_transform.xx <= 0.5:
+        if self.zoom_transform.xx <= 0.05:
             scale = max(scale, 1)
-        elif self.zoom_transform.xx >= 16:
+        elif self.zoom_transform.xx >= 32:
             scale = min(scale, 1)
 
         # undo the scaled translation that the old zoom transform did
diff --git a/go.mod b/go.mod
index 09c71f4..b1720c2 100644
--- a/go.mod
+++ b/go.mod
@@ -7,27 +7,32 @@
 	github.com/golang/protobuf v1.5.2
 	github.com/google/flatbuffers v2.0.5+incompatible
 	google.golang.org/grpc v1.43.0
+	gorm.io/driver/postgres v1.3.7
+	gorm.io/gorm v1.23.5
 )
 
 require (
 	github.com/cenkalti/backoff v2.2.1+incompatible // indirect
 	github.com/davecgh/go-spew v1.1.1
 	github.com/google/go-querystring v1.1.0 // indirect
-	github.com/jackc/pgx v3.6.2+incompatible
 	github.com/phst/runfiles v0.0.0-20220125203201-388095b3a22d
 	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
 	golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
-	golang.org/x/text v0.3.6 // indirect
+	golang.org/x/text v0.3.7 // indirect
 	google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
 	google.golang.org/protobuf v1.26.0 // indirect
 )
 
 require (
-	github.com/cockroachdb/apd v1.1.0 // indirect
-	github.com/gofrs/uuid v4.0.0+incompatible // indirect
-	github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
-	github.com/lib/pq v1.10.2 // indirect
-	github.com/pkg/errors v0.8.1 // indirect
-	github.com/shopspring/decimal v1.2.0 // indirect
-	golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
+	github.com/jackc/chunkreader/v2 v2.0.1 // indirect
+	github.com/jackc/pgconn v1.12.1 // indirect
+	github.com/jackc/pgio v1.0.0 // indirect
+	github.com/jackc/pgpassfile v1.0.0 // indirect
+	github.com/jackc/pgproto3/v2 v2.3.0 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
+	github.com/jackc/pgtype v1.11.0 // indirect
+	github.com/jackc/pgx/v4 v4.16.1 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.4 // indirect
+	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
 )
diff --git a/go.sum b/go.sum
index 7c08101..3082bdc 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,8 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
+github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/buildkite/go-buildkite v2.2.0+incompatible h1:yEjSu1axFC88x4dbufhgMDsEnJztPWlLiZzEvzJggXc=
 github.com/buildkite/go-buildkite v2.2.0+incompatible/go.mod h1:WTV0aX5KnQ9ofsKMg2CLUBLJNsQ0RwOEKPhrXXZWPcE=
@@ -17,6 +19,9 @@
 github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
 github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -27,6 +32,9 @@
 github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
 github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -57,41 +65,145 @@
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
-github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
-github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
-github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
+github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
+github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
+github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
+github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
+github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
+github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
+github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
+github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
+github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
+github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8=
+github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono=
+github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
+github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
+github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
+github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
+github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
+github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
+github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y=
+github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
+github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
+github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
+github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
+github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
+github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
+github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs=
+github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
+github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
+github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
+github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
+github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
+github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y=
+github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ=
+github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
+github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
 github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 github.com/phst/runfiles v0.0.0-20220125203201-388095b3a22d h1:N5aMcF9W9AjW4ed+PJhA7+FjdgPa9gJ+St3mNu2tq1Q=
 github.com/phst/runfiles v0.0.0-20220125203201-388095b3a22d/go.mod h1:+oijTyzCf6Qe7sczsCOuoeX11IxZ+UkXXlhLrfyHlzg=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
+github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
 github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
 github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
+golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -102,22 +214,44 @@
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -149,8 +283,18 @@
 google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ=
+gorm.io/driver/postgres v1.3.7/go.mod h1:f02ympjIcgtHEGFMZvdgTxODZ9snAHDb4hXfigBVuNI=
+gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
+gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
+gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
diff --git a/go_deps.bzl b/go_deps.bzl
index 9d37e42..2869b5e 100644
--- a/go_deps.bzl
+++ b/go_deps.bzl
@@ -5,8 +5,8 @@
     maybe_override_go_dep(
         name = "co_honnef_go_tools",
         importpath = "honnef.co/go/tools",
-        sum = "h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=",
-        version = "v0.0.0-20190523083050-ea95bdfd59fc",
+        sum = "h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=",
+        version = "v0.0.1-2019.2.3",
     )
     maybe_override_go_dep(
         name = "com_github_antihax_optional",
@@ -69,6 +69,18 @@
         version = "v1.1.0",
     )
     maybe_override_go_dep(
+        name = "com_github_coreos_go_systemd",
+        importpath = "github.com/coreos/go-systemd",
+        sum = "h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=",
+        version = "v0.0.0-20190719114852-fd7a80b32e1f",
+    )
+    maybe_override_go_dep(
+        name = "com_github_creack_pty",
+        importpath = "github.com/creack/pty",
+        sum = "h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=",
+        version = "v1.1.7",
+    )
+    maybe_override_go_dep(
         name = "com_github_davecgh_go_spew",
         importpath = "github.com/davecgh/go-spew",
         sum = "h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=",
@@ -93,6 +105,24 @@
         version = "v1.0.0",
     )
     maybe_override_go_dep(
+        name = "com_github_go_kit_log",
+        importpath = "github.com/go-kit/log",
+        sum = "h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ=",
+        version = "v0.1.0",
+    )
+    maybe_override_go_dep(
+        name = "com_github_go_logfmt_logfmt",
+        importpath = "github.com/go-logfmt/logfmt",
+        sum = "h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=",
+        version = "v0.5.0",
+    )
+    maybe_override_go_dep(
+        name = "com_github_go_stack_stack",
+        importpath = "github.com/go-stack/stack",
+        sum = "h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=",
+        version = "v1.8.0",
+    )
+    maybe_override_go_dep(
         name = "com_github_gofrs_uuid",
         importpath = "github.com/gofrs/uuid",
         sum = "h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=",
@@ -129,6 +159,12 @@
         version = "v1.1.0",
     )
     maybe_override_go_dep(
+        name = "com_github_google_renameio",
+        importpath = "github.com/google/renameio",
+        sum = "h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=",
+        version = "v0.1.0",
+    )
+    maybe_override_go_dep(
         name = "com_github_google_uuid",
         importpath = "github.com/google/uuid",
         sum = "h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=",
@@ -141,16 +177,118 @@
         version = "v1.16.0",
     )
     maybe_override_go_dep(
-        name = "com_github_jackc_fake",
-        importpath = "github.com/jackc/fake",
-        sum = "h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=",
-        version = "v0.0.0-20150926172116-812a484cc733",
+        name = "com_github_jackc_chunkreader",
+        importpath = "github.com/jackc/chunkreader",
+        sum = "h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=",
+        version = "v1.0.0",
     )
     maybe_override_go_dep(
-        name = "com_github_jackc_pgx",
-        importpath = "github.com/jackc/pgx",
-        sum = "h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=",
-        version = "v3.6.2+incompatible",
+        name = "com_github_jackc_chunkreader_v2",
+        importpath = "github.com/jackc/chunkreader/v2",
+        sum = "h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=",
+        version = "v2.0.1",
+    )
+    maybe_override_go_dep(
+        name = "com_github_jackc_pgconn",
+        importpath = "github.com/jackc/pgconn",
+        sum = "h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8=",
+        version = "v1.12.1",
+    )
+    maybe_override_go_dep(
+        name = "com_github_jackc_pgio",
+        importpath = "github.com/jackc/pgio",
+        sum = "h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=",
+        version = "v1.0.0",
+    )
+    maybe_override_go_dep(
+        name = "com_github_jackc_pgmock",
+        importpath = "github.com/jackc/pgmock",
+        sum = "h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=",
+        version = "v0.0.0-20210724152146-4ad1a8207f65",
+    )
+    maybe_override_go_dep(
+        name = "com_github_jackc_pgpassfile",
+        importpath = "github.com/jackc/pgpassfile",
+        sum = "h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=",
+        version = "v1.0.0",
+    )
+    maybe_override_go_dep(
+        name = "com_github_jackc_pgproto3",
+        importpath = "github.com/jackc/pgproto3",
+        sum = "h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=",
+        version = "v1.1.0",
+    )
+    maybe_override_go_dep(
+        name = "com_github_jackc_pgproto3_v2",
+        importpath = "github.com/jackc/pgproto3/v2",
+        sum = "h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y=",
+        version = "v2.3.0",
+    )
+    maybe_override_go_dep(
+        name = "com_github_jackc_pgservicefile",
+        importpath = "github.com/jackc/pgservicefile",
+        sum = "h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=",
+        version = "v0.0.0-20200714003250-2b9c44734f2b",
+    )
+    maybe_override_go_dep(
+        name = "com_github_jackc_pgtype",
+        importpath = "github.com/jackc/pgtype",
+        sum = "h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs=",
+        version = "v1.11.0",
+    )
+    maybe_override_go_dep(
+        name = "com_github_jackc_pgx_v4",
+        importpath = "github.com/jackc/pgx/v4",
+        sum = "h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y=",
+        version = "v4.16.1",
+    )
+    maybe_override_go_dep(
+        name = "com_github_jackc_puddle",
+        importpath = "github.com/jackc/puddle",
+        sum = "h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw=",
+        version = "v1.2.1",
+    )
+    maybe_override_go_dep(
+        name = "com_github_jinzhu_inflection",
+        importpath = "github.com/jinzhu/inflection",
+        sum = "h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=",
+        version = "v1.0.0",
+    )
+    maybe_override_go_dep(
+        name = "com_github_jinzhu_now",
+        importpath = "github.com/jinzhu/now",
+        sum = "h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=",
+        version = "v1.1.4",
+    )
+    maybe_override_go_dep(
+        name = "com_github_kisielk_gotool",
+        importpath = "github.com/kisielk/gotool",
+        sum = "h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=",
+        version = "v1.0.0",
+    )
+    maybe_override_go_dep(
+        name = "com_github_konsorten_go_windows_terminal_sequences",
+        importpath = "github.com/konsorten/go-windows-terminal-sequences",
+        sum = "h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=",
+        version = "v1.0.2",
+    )
+    maybe_override_go_dep(
+        name = "com_github_kr_pretty",
+        importpath = "github.com/kr/pretty",
+        sum = "h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=",
+        version = "v0.1.0",
+    )
+    maybe_override_go_dep(
+        name = "com_github_kr_pty",
+        importpath = "github.com/kr/pty",
+        sum = "h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=",
+        version = "v1.1.8",
+    )
+    maybe_override_go_dep(
+        name = "com_github_kr_text",
+        importpath = "github.com/kr/text",
+        sum = "h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=",
+        version = "v0.1.0",
     )
     maybe_override_go_dep(
         name = "com_github_lib_pq",
@@ -159,6 +297,24 @@
         version = "v1.10.2",
     )
     maybe_override_go_dep(
+        name = "com_github_masterminds_semver_v3",
+        importpath = "github.com/Masterminds/semver/v3",
+        sum = "h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=",
+        version = "v3.1.1",
+    )
+    maybe_override_go_dep(
+        name = "com_github_mattn_go_colorable",
+        importpath = "github.com/mattn/go-colorable",
+        sum = "h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=",
+        version = "v0.1.6",
+    )
+    maybe_override_go_dep(
+        name = "com_github_mattn_go_isatty",
+        importpath = "github.com/mattn/go-isatty",
+        sum = "h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=",
+        version = "v0.0.12",
+    )
+    maybe_override_go_dep(
         name = "com_github_phst_runfiles",
         importpath = "github.com/phst/runfiles",
         sum = "h1:N5aMcF9W9AjW4ed+PJhA7+FjdgPa9gJ+St3mNu2tq1Q=",
@@ -189,16 +345,46 @@
         version = "v1.2.0",
     )
     maybe_override_go_dep(
+        name = "com_github_rogpeppe_go_internal",
+        importpath = "github.com/rogpeppe/go-internal",
+        sum = "h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=",
+        version = "v1.3.0",
+    )
+    maybe_override_go_dep(
+        name = "com_github_rs_xid",
+        importpath = "github.com/rs/xid",
+        sum = "h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=",
+        version = "v1.2.1",
+    )
+    maybe_override_go_dep(
+        name = "com_github_rs_zerolog",
+        importpath = "github.com/rs/zerolog",
+        sum = "h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=",
+        version = "v1.15.0",
+    )
+    maybe_override_go_dep(
+        name = "com_github_satori_go_uuid",
+        importpath = "github.com/satori/go.uuid",
+        sum = "h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=",
+        version = "v1.2.0",
+    )
+    maybe_override_go_dep(
         name = "com_github_shopspring_decimal",
         importpath = "github.com/shopspring/decimal",
         sum = "h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=",
         version = "v1.2.0",
     )
     maybe_override_go_dep(
+        name = "com_github_sirupsen_logrus",
+        importpath = "github.com/sirupsen/logrus",
+        sum = "h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=",
+        version = "v1.4.2",
+    )
+    maybe_override_go_dep(
         name = "com_github_stretchr_objx",
         importpath = "github.com/stretchr/objx",
-        sum = "h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=",
-        version = "v0.1.0",
+        sum = "h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=",
+        version = "v0.2.0",
     )
     maybe_override_go_dep(
         name = "com_github_stretchr_testify",
@@ -207,6 +393,12 @@
         version = "v1.7.0",
     )
     maybe_override_go_dep(
+        name = "com_github_zenazn_goji",
+        importpath = "github.com/zenazn/goji",
+        sum = "h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ=",
+        version = "v0.9.0",
+    )
+    maybe_override_go_dep(
         name = "com_google_cloud_go",
         importpath = "cloud.google.com/go",
         sum = "h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=",
@@ -215,8 +407,20 @@
     maybe_override_go_dep(
         name = "in_gopkg_check_v1",
         importpath = "gopkg.in/check.v1",
-        sum = "h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=",
-        version = "v0.0.0-20161208181325-20d25e280405",
+        sum = "h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=",
+        version = "v1.0.0-20180628173108-788fd7840127",
+    )
+    maybe_override_go_dep(
+        name = "in_gopkg_errgo_v2",
+        importpath = "gopkg.in/errgo.v2",
+        sum = "h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=",
+        version = "v2.1.0",
+    )
+    maybe_override_go_dep(
+        name = "in_gopkg_inconshreveable_log15_v2",
+        importpath = "gopkg.in/inconshreveable/log15.v2",
+        sum = "h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A=",
+        version = "v2.0.0-20180818164646-67afb5ed74ec",
     )
     maybe_override_go_dep(
         name = "in_gopkg_yaml_v2",
@@ -231,6 +435,18 @@
         version = "v3.0.0-20200313102051-9f266ea9e77c",
     )
     maybe_override_go_dep(
+        name = "io_gorm_driver_postgres",
+        importpath = "gorm.io/driver/postgres",
+        sum = "h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ=",
+        version = "v1.3.7",
+    )
+    maybe_override_go_dep(
+        name = "io_gorm_gorm",
+        importpath = "gorm.io/gorm",
+        sum = "h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=",
+        version = "v1.23.5",
+    )
+    maybe_override_go_dep(
         name = "io_opentelemetry_go_proto_otlp",
         importpath = "go.opentelemetry.io/proto/otlp",
         sum = "h1:rwOQPCuKAKmwGKq2aVNnYIibI6wnV7EvzgfTCzcdGg8=",
@@ -263,8 +479,8 @@
     maybe_override_go_dep(
         name = "org_golang_x_crypto",
         importpath = "golang.org/x/crypto",
-        sum = "h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=",
-        version = "v0.0.0-20210711020723-a769d52b0f97",
+        sum = "h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=",
+        version = "v0.0.0-20210921155107-089bfa567519",
     )
     maybe_override_go_dep(
         name = "org_golang_x_exp",
@@ -275,8 +491,14 @@
     maybe_override_go_dep(
         name = "org_golang_x_lint",
         importpath = "golang.org/x/lint",
-        sum = "h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=",
-        version = "v0.0.0-20190313153728-d0100b6bd8b3",
+        sum = "h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=",
+        version = "v0.0.0-20190930215403-16217165b5de",
+    )
+    maybe_override_go_dep(
+        name = "org_golang_x_mod",
+        importpath = "golang.org/x/mod",
+        sum = "h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=",
+        version = "v0.1.1-0.20191105210325-c90efee705ee",
     )
     maybe_override_go_dep(
         name = "org_golang_x_net",
@@ -311,14 +533,14 @@
     maybe_override_go_dep(
         name = "org_golang_x_text",
         importpath = "golang.org/x/text",
-        sum = "h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=",
-        version = "v0.3.6",
+        sum = "h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=",
+        version = "v0.3.7",
     )
     maybe_override_go_dep(
         name = "org_golang_x_tools",
         importpath = "golang.org/x/tools",
-        sum = "h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=",
-        version = "v0.0.0-20190524140312-2c0ae7006135",
+        sum = "h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE=",
+        version = "v0.0.0-20200103221440-774c71fcf114",
     )
     maybe_override_go_dep(
         name = "org_golang_x_xerrors",
@@ -326,3 +548,27 @@
         sum = "h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=",
         version = "v0.0.0-20200804184101-5ec99f83aff1",
     )
+    maybe_override_go_dep(
+        name = "org_uber_go_atomic",
+        importpath = "go.uber.org/atomic",
+        sum = "h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=",
+        version = "v1.6.0",
+    )
+    maybe_override_go_dep(
+        name = "org_uber_go_multierr",
+        importpath = "go.uber.org/multierr",
+        sum = "h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=",
+        version = "v1.5.0",
+    )
+    maybe_override_go_dep(
+        name = "org_uber_go_tools",
+        importpath = "go.uber.org/tools",
+        sum = "h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=",
+        version = "v0.0.0-20190618225709-2cfd321de3ee",
+    )
+    maybe_override_go_dep(
+        name = "org_uber_go_zap",
+        importpath = "go.uber.org/zap",
+        sum = "h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=",
+        version = "v1.13.0",
+    )
diff --git a/scouting/db/BUILD b/scouting/db/BUILD
index 7fbd2e2..154cab6 100644
--- a/scouting/db/BUILD
+++ b/scouting/db/BUILD
@@ -6,7 +6,12 @@
     importpath = "github.com/frc971/971-Robot-Code/scouting/db",
     target_compatible_with = ["@platforms//cpu:x86_64"],
     visibility = ["//visibility:public"],
-    deps = ["@com_github_jackc_pgx//stdlib"],
+    deps = [
+        "@io_gorm_driver_postgres//:postgres",
+        "@io_gorm_gorm//:gorm",
+        "@io_gorm_gorm//clause",
+        "@io_gorm_gorm//logger",
+    ],
 )
 
 go_test(
@@ -18,4 +23,5 @@
     ],
     embed = [":db"],
     target_compatible_with = ["@platforms//cpu:x86_64"],
+    deps = ["@com_github_davecgh_go_spew//spew"],
 )
diff --git a/scouting/db/db.go b/scouting/db/db.go
index 3d514e3..82f7fa8 100644
--- a/scouting/db/db.go
+++ b/scouting/db/db.go
@@ -1,33 +1,50 @@
 package db
 
 import (
-	"database/sql"
 	"errors"
 	"fmt"
 
-	_ "github.com/jackc/pgx/stdlib"
+	"gorm.io/driver/postgres"
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/logger"
 )
 
 type Database struct {
-	*sql.DB
+	*gorm.DB
 }
 
 type Match struct {
-	MatchNumber, SetNumber int32
-	CompLevel              string
+	// TODO(phil): Rework this be be one team per row.
+	// Makes queries much simpler.
+	MatchNumber            int32  `gorm:"primaryKey"`
+	SetNumber              int32  `gorm:"primaryKey"`
+	CompLevel              string `gorm:"primaryKey"`
 	R1, R2, R3, B1, B2, B3 int32
 }
 
 type Shift struct {
-	MatchNumber                                                      int32
+	MatchNumber                                                      int32 `gorm:"primaryKey"`
 	R1scouter, R2scouter, R3scouter, B1scouter, B2scouter, B3scouter string
 }
 
 type Stats struct {
-	TeamNumber, MatchNumber, SetNumber int32
-	CompLevel                          string
-	StartingQuadrant                   int32
-	AutoBallPickedUp                   [5]bool
+	TeamNumber       int32  `gorm:"primaryKey"`
+	MatchNumber      int32  `gorm:"primaryKey"`
+	SetNumber        int32  `gorm:"primaryKey"`
+	CompLevel        string `gorm:"primaryKey"`
+	StartingQuadrant int32
+	// This field is for the balls picked up during auto. Use this field
+	// when using this library. Ignore the AutoBallPickedUpX fields below.
+	AutoBallPickedUp [5]bool `gorm:"-:all"`
+	// These fields are internal implementation details. Do not use these.
+	// TODO(phil): Figure out how to use the JSON gorm serializer instead
+	// of manually serializing/deserializing these.
+	AutoBallPickedUp1 bool
+	AutoBallPickedUp2 bool
+	AutoBallPickedUp3 bool
+	AutoBallPickedUp4 bool
+	AutoBallPickedUp5 bool
 	// TODO(phil): Re-order auto and teleop fields so auto comes first.
 	ShotsMissed, UpperGoalShots, LowerGoalShots   int32
 	ShotsMissedAuto, UpperGoalAuto, LowerGoalAuto int32
@@ -50,12 +67,19 @@
 }
 
 type NotesData struct {
-	TeamNumber int32
-	Notes      []string
+	ID           uint `gorm:"primaryKey"`
+	TeamNumber   int32
+	Notes        string
+	GoodDriving  bool
+	BadDriving   bool
+	SketchyClimb bool
+	SolidClimb   bool
+	GoodDefense  bool
+	BadDefense   bool
 }
 
 type Ranking struct {
-	TeamNumber         int
+	TeamNumber         int `gorm:"primaryKey"`
 	Losses, Wins, Ties int32
 	Rank, Dq           int32
 }
@@ -66,217 +90,48 @@
 	var err error
 	database := new(Database)
 
-	psqlInfo := fmt.Sprintf("postgres://%s:%s@localhost:%d/postgres", user, password, port)
-	database.DB, err = sql.Open("pgx", psqlInfo)
+	dsn := fmt.Sprintf("host=localhost user=%s password=%s dbname=postgres port=%d sslmode=disable", user, password, port)
+	database.DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
+		Logger: logger.Default.LogMode(logger.Silent),
+	})
 	if err != nil {
+		database.Delete()
 		return nil, errors.New(fmt.Sprint("Failed to connect to postgres: ", err))
 	}
 
-	statement, err := database.Prepare("CREATE TABLE IF NOT EXISTS matches (" +
-		"MatchNumber INTEGER, " +
-		"SetNumber INTEGER, " +
-		"CompLevel VARCHAR, " +
-		"R1 INTEGER, " +
-		"R2 INTEGER, " +
-		"R3 INTEGER, " +
-		"B1 INTEGER, " +
-		"B2 INTEGER, " +
-		"B3 INTEGER, " +
-		"PRIMARY KEY (MatchNumber, SetNumber, CompLevel))")
+	err = database.AutoMigrate(&Match{}, &Shift{}, &Stats{}, &NotesData{}, &Ranking{})
 	if err != nil {
-		database.Close()
-		return nil, errors.New(fmt.Sprint("Failed to prepare matches table creation: ", err))
-	}
-	defer statement.Close()
-
-	_, err = statement.Exec()
-	if err != nil {
-		database.Close()
-		return nil, errors.New(fmt.Sprint("Failed to create matches table: ", err))
-	}
-
-	statement, err = database.Prepare("CREATE TABLE IF NOT EXISTS shift_schedule (" +
-		"id SERIAL PRIMARY KEY, " +
-		"MatchNumber INTEGER, " +
-		"R1Scouter VARCHAR, " +
-		"R2Scouter VARCHAR, " +
-		"R3Scouter VARCHAR, " +
-		"B1Scouter VARCHAR, " +
-		"B2Scouter VARCHAR, " +
-		"B3scouter VARCHAR)")
-	if err != nil {
-		database.Close()
-		return nil, errors.New(fmt.Sprint("Failed to prepare shift schedule table creation: ", err))
-	}
-	defer statement.Close()
-
-	_, err = statement.Exec()
-	if err != nil {
-		database.Close()
-		return nil, errors.New(fmt.Sprint("Failed to create shift schedule table: ", err))
-	}
-
-	statement, err = database.Prepare("CREATE TABLE IF NOT EXISTS team_match_stats (" +
-		"TeamNumber INTEGER, " +
-		"MatchNumber INTEGER, " +
-		"SetNumber INTEGER, " +
-		"CompLevel VARCHAR, " +
-		"StartingQuadrant INTEGER, " +
-		"AutoBall1PickedUp BOOLEAN, " +
-		"AutoBall2PickedUp BOOLEAN, " +
-		"AutoBall3PickedUp BOOLEAN, " +
-		"AutoBall4PickedUp BOOLEAN, " +
-		"AutoBall5PickedUp BOOLEAN, " +
-		"ShotsMissed INTEGER, " +
-		"UpperGoalShots INTEGER, " +
-		"LowerGoalShots INTEGER, " +
-		"ShotsMissedAuto INTEGER, " +
-		"UpperGoalAuto INTEGER, " +
-		"LowerGoalAuto INTEGER, " +
-		"PlayedDefense INTEGER, " +
-		"DefenseReceivedScore INTEGER, " +
-		"Climbing INTEGER, " +
-		"Comment VARCHAR, " +
-		"CollectedBy VARCHAR, " +
-		"PRIMARY KEY (TeamNumber, MatchNumber, SetNumber, CompLevel))")
-	if err != nil {
-		database.Close()
-		return nil, errors.New(fmt.Sprint("Failed to prepare stats table creation: ", err))
-	}
-	defer statement.Close()
-
-	_, err = statement.Exec()
-	if err != nil {
-		database.Close()
-		return nil, errors.New(fmt.Sprint("Failed to create team_match_stats table: ", err))
-	}
-
-	statement, err = database.Prepare("CREATE TABLE IF NOT EXISTS team_notes (" +
-		"id SERIAL PRIMARY KEY, " +
-		"TeamNumber INTEGER, " +
-		"Notes TEXT)")
-	if err != nil {
-		return nil, errors.New(fmt.Sprint("Failed to prepare notes table creation: ", err))
-	}
-	defer statement.Close()
-
-	_, err = statement.Exec()
-	if err != nil {
-		return nil, errors.New(fmt.Sprint("Failed to create notes table: ", err))
-	}
-
-	statement, err = database.Prepare("CREATE TABLE IF NOT EXISTS rankings (" +
-		"id SERIAL PRIMARY KEY, " +
-		"Losses INTEGER, " +
-		"Wins INTEGER, " +
-		"Ties INTEGER, " +
-		"Rank INTEGER, " +
-		"Dq INTEGER, " +
-		"TeamNumber INTEGER)")
-	if err != nil {
-		return nil, errors.New(fmt.Sprint("Failed to prepare rankings table creation: ", err))
-	}
-	defer statement.Close()
-
-	_, err = statement.Exec()
-	if err != nil {
-		return nil, errors.New(fmt.Sprint("Failed to create rankings table: ", err))
+		database.Delete()
+		return nil, errors.New(fmt.Sprint("Failed to create/migrate tables: ", err))
 	}
 
 	return database, nil
 }
 
 func (database *Database) Delete() error {
-	statement, err := database.Prepare("DROP TABLE IF EXISTS matches")
+	sql, err := database.DB.DB()
 	if err != nil {
-		return errors.New(fmt.Sprint("Failed to prepare dropping matches table: ", err))
+		return err
 	}
-	_, err = statement.Exec()
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to drop matches table: ", err))
-	}
-
-	statement, err = database.Prepare("DROP TABLE IF EXISTS shift_schedule")
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to prepare dropping shifts table: ", err))
-	}
-	_, err = statement.Exec()
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to drop shifts table: ", err))
-	}
-
-	statement, err = database.Prepare("DROP TABLE IF EXISTS team_match_stats")
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to prepare dropping stats table: ", err))
-	}
-	_, err = statement.Exec()
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to drop stats table: ", err))
-	}
-
-	statement, err = database.Prepare("DROP TABLE IF EXISTS team_notes")
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to prepare dropping notes table: ", err))
-	}
-	_, err = statement.Exec()
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to drop notes table: ", err))
-	}
-	return nil
-
-	statement, err = database.Prepare("DROP TABLE IF EXISTS rankings")
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to prepare dropping rankings table: ", err))
-	}
-	_, err = statement.Exec()
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to drop rankings table: ", err))
-	}
-	return nil
+	return sql.Close()
 }
 
-// This function will also populate the Stats table with six empty rows every time a match is added
-func (database *Database) AddToMatch(m Match) error {
-	statement, err := database.Prepare("INSERT INTO matches(" +
-		"MatchNumber, SetNumber, CompLevel, " +
-		"R1, R2, R3, B1, B2, B3) " +
-		"VALUES (" +
-		"$1, $2, $3, " +
-		"$4, $5, $6, $7, $8, $9) " +
-		"ON CONFLICT (MatchNumber, SetNumber, CompLevel) DO UPDATE SET " +
-		"R1 = EXCLUDED.R1, R2 = EXCLUDED.R2, R3 = EXCLUDED.R3, " +
-		"B1 = EXCLUDED.B1, B2 = EXCLUDED.B2, B3 = EXCLUDED.B3")
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to prepare insertion into match database: ", err))
-	}
-	defer statement.Close()
+func (database *Database) SetDebugLogLevel() {
+	database.DB.Logger = database.DB.Logger.LogMode(logger.Info)
+}
 
-	_, err = statement.Exec(m.MatchNumber, m.SetNumber, m.CompLevel,
-		m.R1, m.R2, m.R3, m.B1, m.B2, m.B3)
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to insert into match database: ", err))
-	}
-	return nil
+func (database *Database) AddToMatch(m Match) error {
+	result := database.Clauses(clause.OnConflict{
+		UpdateAll: true,
+	}).Create(&m)
+	return result.Error
 }
 
 func (database *Database) AddToShift(sh Shift) error {
-	statement, err := database.Prepare("INSERT INTO shift_schedule(" +
-		"MatchNumber, " +
-		"R1scouter, R2scouter, R3scouter, B1scouter, B2scouter, B3scouter) " +
-		"VALUES (" +
-		"$1, " +
-		"$2, $3, $4, $5, $6, $7)")
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to prepare insertion into shift database: ", err))
-	}
-	defer statement.Close()
-
-	_, err = statement.Exec(sh.MatchNumber,
-		sh.R1scouter, sh.R2scouter, sh.R3scouter, sh.B1scouter, sh.B2scouter, sh.B3scouter)
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to insert into shift database: ", err))
-	}
-	return nil
+	result := database.Clauses(clause.OnConflict{
+		UpdateAll: true,
+	}).Create(&sh)
+	return result.Error
 }
 
 func (database *Database) AddToStats(s Stats) error {
@@ -297,304 +152,123 @@
 			" in match ", s.MatchNumber, " in the schedule."))
 	}
 
-	statement, err := database.Prepare("INSERT INTO team_match_stats(" +
-		"TeamNumber, MatchNumber, SetNumber, CompLevel, " +
-		"StartingQuadrant, " +
-		"AutoBall1PickedUp, AutoBall2PickedUp, AutoBall3PickedUp, " +
-		"AutoBall4PickedUp, AutoBall5PickedUp, " +
-		"ShotsMissed, UpperGoalShots, LowerGoalShots, " +
-		"ShotsMissedAuto, UpperGoalAuto, LowerGoalAuto, " +
-		"PlayedDefense, DefenseReceivedScore, Climbing, " +
-		"Comment, CollectedBy) " +
-		"VALUES (" +
-		"$1, $2, $3, $4, " +
-		"$5, " +
-		"$6, $7, $8, " +
-		"$9, $10, " +
-		"$11, $12, $13, " +
-		"$14, $15, $16, " +
-		"$17, $18, $19, " +
-		"$20, $21)")
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to prepare stats update statement: ", err))
-	}
-	defer statement.Close()
-
-	_, err = statement.Exec(
-		s.TeamNumber, s.MatchNumber, s.SetNumber, s.CompLevel,
-		s.StartingQuadrant,
-		s.AutoBallPickedUp[0], s.AutoBallPickedUp[1], s.AutoBallPickedUp[2],
-		s.AutoBallPickedUp[3], s.AutoBallPickedUp[4],
-		s.ShotsMissed, s.UpperGoalShots, s.LowerGoalShots,
-		s.ShotsMissedAuto, s.UpperGoalAuto, s.LowerGoalAuto,
-		s.PlayedDefense, s.DefenseReceivedScore, s.Climbing,
-		s.Comment, s.CollectedBy)
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to update stats database: ", err))
-	}
-
-	return nil
+	// Unpack the auto balls array.
+	s.AutoBallPickedUp1 = s.AutoBallPickedUp[0]
+	s.AutoBallPickedUp2 = s.AutoBallPickedUp[1]
+	s.AutoBallPickedUp3 = s.AutoBallPickedUp[2]
+	s.AutoBallPickedUp4 = s.AutoBallPickedUp[3]
+	s.AutoBallPickedUp5 = s.AutoBallPickedUp[4]
+	result := database.Create(&s)
+	return result.Error
 }
 
 func (database *Database) AddOrUpdateRankings(r Ranking) error {
-	statement, err := database.Prepare("UPDATE rankings SET " +
-		"Losses = $1, Wins = $2, Ties = $3, " +
-		"Rank = $4, Dq = $5, TeamNumber = $6 " +
-		"WHERE TeamNumber = $6")
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to prepare rankings database update: ", err))
-	}
-	defer statement.Close()
-
-	result, err := statement.Exec(r.Losses, r.Wins, r.Ties,
-		r.Rank, r.Dq, r.TeamNumber)
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to update rankings database: ", err))
-	}
-
-	numRowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to query rows affected: ", err))
-	}
-	if numRowsAffected == 0 {
-		statement, err := database.Prepare("INSERT INTO rankings(" +
-			"Losses, Wins, Ties, " +
-			"Rank, Dq, TeamNumber) " +
-			"VALUES (" +
-			"$1, $2, $3, " +
-			"$4, $5, $6)")
-		if err != nil {
-			return errors.New(fmt.Sprint("Failed to prepare insertion into rankings database: ", err))
-		}
-		defer statement.Close()
-
-		_, err = statement.Exec(r.Losses, r.Wins, r.Ties,
-			r.Rank, r.Dq, r.TeamNumber)
-		if err != nil {
-			return errors.New(fmt.Sprint("Failed to insert into rankings database: ", err))
-		}
-	}
-
-	return nil
+	result := database.Clauses(clause.OnConflict{
+		UpdateAll: true,
+	}).Create(&r)
+	return result.Error
 }
 
 func (database *Database) ReturnMatches() ([]Match, error) {
-	rows, err := database.Query("SELECT * FROM matches")
-	if err != nil {
-		return nil, errors.New(fmt.Sprint("Failed to select from matches: ", err))
-	}
-	defer rows.Close()
-
-	matches := make([]Match, 0)
-	for rows.Next() {
-		var match Match
-		err := rows.Scan(&match.MatchNumber, &match.SetNumber, &match.CompLevel,
-			&match.R1, &match.R2, &match.R3, &match.B1, &match.B2, &match.B3)
-		if err != nil {
-			return nil, errors.New(fmt.Sprint("Failed to scan from matches: ", err))
-		}
-		matches = append(matches, match)
-	}
-	return matches, nil
+	var matches []Match
+	result := database.Find(&matches)
+	return matches, result.Error
 }
 
 func (database *Database) ReturnAllShifts() ([]Shift, error) {
-	rows, err := database.Query("SELECT * FROM shift_schedule")
-	if err != nil {
-		return nil, errors.New(fmt.Sprint("Failed to select from shift: ", err))
-	}
-	defer rows.Close()
+	var shifts []Shift
+	result := database.Find(&shifts)
+	return shifts, result.Error
+}
 
-	shifts := make([]Shift, 0)
-	for rows.Next() {
-		var shift Shift
-		var id int
-		err := rows.Scan(&id, &shift.MatchNumber,
-			&shift.R1scouter, &shift.R2scouter, &shift.R3scouter, &shift.B1scouter, &shift.B2scouter, &shift.B3scouter)
-		if err != nil {
-			return nil, errors.New(fmt.Sprint("Failed to scan from shift: ", err))
-		}
-		shifts = append(shifts, shift)
+// Packs the stats. This really just consists of taking the individual auto
+// ball booleans and turning them into an array. The individual booleans are
+// cleared so that they don't affect struct comparisons.
+func packStats(stats *Stats) {
+	stats.AutoBallPickedUp = [5]bool{
+		stats.AutoBallPickedUp1,
+		stats.AutoBallPickedUp2,
+		stats.AutoBallPickedUp3,
+		stats.AutoBallPickedUp4,
+		stats.AutoBallPickedUp5,
 	}
-	return shifts, nil
+	stats.AutoBallPickedUp1 = false
+	stats.AutoBallPickedUp2 = false
+	stats.AutoBallPickedUp3 = false
+	stats.AutoBallPickedUp4 = false
+	stats.AutoBallPickedUp5 = false
 }
 
 func (database *Database) ReturnStats() ([]Stats, error) {
-	rows, err := database.Query("SELECT * FROM team_match_stats")
-	if err != nil {
-		return nil, errors.New(fmt.Sprint("Failed to SELECT * FROM team_match_stats: ", err))
+	var stats []Stats
+	result := database.Find(&stats)
+	// Pack the auto balls array.
+	for i := range stats {
+		packStats(&stats[i])
 	}
-	defer rows.Close()
-
-	teams := make([]Stats, 0)
-	for rows.Next() {
-		var team Stats
-		err = rows.Scan(
-			&team.TeamNumber, &team.MatchNumber, &team.SetNumber, &team.CompLevel,
-			&team.StartingQuadrant,
-			&team.AutoBallPickedUp[0], &team.AutoBallPickedUp[1], &team.AutoBallPickedUp[2],
-			&team.AutoBallPickedUp[3], &team.AutoBallPickedUp[4],
-			&team.ShotsMissed, &team.UpperGoalShots, &team.LowerGoalShots,
-			&team.ShotsMissedAuto, &team.UpperGoalAuto, &team.LowerGoalAuto,
-			&team.PlayedDefense, &team.DefenseReceivedScore, &team.Climbing,
-			&team.Comment, &team.CollectedBy)
-		if err != nil {
-			return nil, errors.New(fmt.Sprint("Failed to scan from stats: ", err))
-		}
-		teams = append(teams, team)
-	}
-	return teams, nil
+	return stats, result.Error
 }
 
 func (database *Database) ReturnRankings() ([]Ranking, error) {
-	rows, err := database.Query("SELECT * FROM rankings")
-	if err != nil {
-		return nil, errors.New(fmt.Sprint("Failed to SELECT * FROM rankings: ", err))
-	}
-	defer rows.Close()
-
-	all_rankings := make([]Ranking, 0)
-	for rows.Next() {
-		var ranking Ranking
-		var id int
-		err = rows.Scan(&id,
-			&ranking.Losses, &ranking.Wins, &ranking.Ties,
-			&ranking.Rank, &ranking.Dq, &ranking.TeamNumber)
-		if err != nil {
-			return nil, errors.New(fmt.Sprint("Failed to scan from rankings: ", err))
-		}
-		all_rankings = append(all_rankings, ranking)
-	}
-	return all_rankings, nil
+	var rankins []Ranking
+	result := database.Find(&rankins)
+	return rankins, result.Error
 }
 
 func (database *Database) QueryMatches(teamNumber_ int32) ([]Match, error) {
-	rows, err := database.Query("SELECT * FROM matches WHERE "+
-		"R1 = $1 OR R2 = $2 OR R3 = $3 OR B1 = $4 OR B2 = $5 OR B3 = $6",
-		teamNumber_, teamNumber_, teamNumber_, teamNumber_, teamNumber_, teamNumber_)
-	if err != nil {
-		return nil, errors.New(fmt.Sprint("Failed to select from matches for team: ", err))
-	}
-	defer rows.Close()
-
 	var matches []Match
-	for rows.Next() {
-		var match Match
-		err = rows.Scan(&match.MatchNumber, &match.SetNumber, &match.CompLevel,
-			&match.R1, &match.R2, &match.R3, &match.B1, &match.B2, &match.B3)
-		if err != nil {
-			return nil, errors.New(fmt.Sprint("Failed to scan from matches: ", err))
-		}
-		matches = append(matches, match)
-	}
-	return matches, nil
+	result := database.
+		Where("r1 = $1 OR r2 = $1 OR r3 = $1 OR b1 = $1 OR b2 = $1 OR b3 = $1", teamNumber_).
+		Find(&matches)
+	return matches, result.Error
 }
 
 func (database *Database) QueryAllShifts(matchNumber_ int) ([]Shift, error) {
-	rows, err := database.Query("SELECT * FROM shift_schedule WHERE MatchNumber = $1", matchNumber_)
-	if err != nil {
-		return nil, errors.New(fmt.Sprint("Failed to select from shift for team: ", err))
-	}
-	defer rows.Close()
-
 	var shifts []Shift
-	for rows.Next() {
-		var shift Shift
-		var id int
-		err = rows.Scan(&id, &shift.MatchNumber,
-			&shift.R1scouter, &shift.R2scouter, &shift.R3scouter, &shift.B1scouter, &shift.B2scouter, &shift.B3scouter)
-		if err != nil {
-			return nil, errors.New(fmt.Sprint("Failed to scan from matches: ", err))
-		}
-		shifts = append(shifts, shift)
-	}
-	return shifts, nil
+	result := database.Where("match_number = ?", matchNumber_).Find(&shifts)
+	return shifts, result.Error
 }
 
 func (database *Database) QueryStats(teamNumber_ int) ([]Stats, error) {
-	rows, err := database.Query("SELECT * FROM team_match_stats WHERE TeamNumber = $1", teamNumber_)
-	if err != nil {
-		return nil, errors.New(fmt.Sprint("Failed to select from stats: ", err))
+	var stats []Stats
+	result := database.Where("team_number = ?", teamNumber_).Find(&stats)
+	// Pack the auto balls array.
+	for i := range stats {
+		packStats(&stats[i])
 	}
-	defer rows.Close()
-
-	var teams []Stats
-	for rows.Next() {
-		var team Stats
-		err = rows.Scan(
-			&team.TeamNumber, &team.MatchNumber, &team.SetNumber, &team.CompLevel,
-			&team.StartingQuadrant,
-			&team.AutoBallPickedUp[0], &team.AutoBallPickedUp[1], &team.AutoBallPickedUp[2],
-			&team.AutoBallPickedUp[3], &team.AutoBallPickedUp[4],
-			&team.ShotsMissed, &team.UpperGoalShots, &team.LowerGoalShots,
-			&team.ShotsMissedAuto, &team.UpperGoalAuto, &team.LowerGoalAuto,
-			&team.PlayedDefense, &team.DefenseReceivedScore, &team.Climbing,
-			&team.Comment, &team.CollectedBy)
-		if err != nil {
-			return nil, errors.New(fmt.Sprint("Failed to scan from stats: ", err))
-		}
-		teams = append(teams, team)
-	}
-	return teams, nil
+	return stats, result.Error
 }
 
-func (database *Database) QueryNotes(TeamNumber int32) (NotesData, error) {
-	rows, err := database.Query("SELECT * FROM team_notes WHERE TeamNumber = $1", TeamNumber)
-	if err != nil {
-		return NotesData{}, errors.New(fmt.Sprint("Failed to select from notes: ", err))
+func (database *Database) QueryNotes(TeamNumber int32) ([]string, error) {
+	var rawNotes []NotesData
+	result := database.Where("team_number = ?", TeamNumber).Find(&rawNotes)
+	if result.Error != nil {
+		return nil, result.Error
 	}
-	defer rows.Close()
 
-	var notes []string
-	for rows.Next() {
-		var id int32
-		var data string
-		err = rows.Scan(&id, &TeamNumber, &data)
-		if err != nil {
-			return NotesData{}, errors.New(fmt.Sprint("Failed to scan from notes: ", err))
-		}
-		notes = append(notes, data)
+	notes := make([]string, len(rawNotes))
+	for i := range rawNotes {
+		notes[i] = rawNotes[i].Notes
 	}
-	return NotesData{TeamNumber, notes}, nil
+	return notes, nil
 }
 
 func (database *Database) QueryRankings(TeamNumber int) ([]Ranking, error) {
-	rows, err := database.Query("SELECT * FROM rankings WHERE TeamNumber = $1", TeamNumber)
-	if err != nil {
-		return nil, errors.New(fmt.Sprint("Failed to select from rankings: ", err))
-	}
-	defer rows.Close()
-
-	all_rankings := make([]Ranking, 0)
-	for rows.Next() {
-		var ranking Ranking
-		var id int
-		err = rows.Scan(&id,
-			&ranking.Losses, &ranking.Wins, &ranking.Ties,
-			&ranking.Rank, &ranking.Dq, &ranking.TeamNumber)
-		if err != nil {
-			return nil, errors.New(fmt.Sprint("Failed to scan from rankings: ", err))
-		}
-		all_rankings = append(all_rankings, ranking)
-	}
-	return all_rankings, nil
+	var rankins []Ranking
+	result := database.Where("team_number = ?", TeamNumber).Find(&rankins)
+	return rankins, result.Error
 }
 
 func (database *Database) AddNotes(data NotesData) error {
-	if len(data.Notes) > 1 {
-		return errors.New("Can only insert one row of notes at a time")
-	}
-	statement, err := database.Prepare("INSERT INTO " +
-		"team_notes(TeamNumber, Notes)" +
-		"VALUES ($1, $2)")
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to prepare insertion into notes table: ", err))
-	}
-	defer statement.Close()
-
-	_, err = statement.Exec(data.TeamNumber, data.Notes[0])
-	if err != nil {
-		return errors.New(fmt.Sprint("Failed to insert into Notes database: ", err))
-	}
-	return nil
+	result := database.Create(&NotesData{
+		TeamNumber:   data.TeamNumber,
+		Notes:        data.Notes,
+		GoodDriving:  data.GoodDriving,
+		BadDriving:   data.BadDriving,
+		SketchyClimb: data.SketchyClimb,
+		SolidClimb:   data.SolidClimb,
+		GoodDefense:  data.GoodDefense,
+		BadDefense:   data.BadDefense,
+	})
+	return result.Error
 }
diff --git a/scouting/db/db_test.go b/scouting/db/db_test.go
index 438e52e..1e11008 100644
--- a/scouting/db/db_test.go
+++ b/scouting/db/db_test.go
@@ -9,6 +9,8 @@
 	"strings"
 	"testing"
 	"time"
+
+	"github.com/davecgh/go-spew/spew"
 )
 
 // Shortcut for error checking. If the specified error is non-nil, print the
@@ -26,7 +28,6 @@
 
 func (fixture dbFixture) TearDown() {
 	fixture.db.Delete()
-	fixture.db.Close()
 	log.Println("Shutting down testdb")
 	fixture.server.Process.Signal(os.Interrupt)
 	fixture.server.Process.Wait()
@@ -55,9 +56,17 @@
 	}
 	log.Println("Connected to postgres.")
 
+	fixture.db.SetDebugLogLevel()
+
 	return fixture
 }
 
+func checkDeepEqual(t *testing.T, expected interface{}, actual interface{}) {
+	if !reflect.DeepEqual(expected, actual) {
+		t.Fatalf(spew.Sprintf("Got %#v,\nbut expected %#v.", actual, expected))
+	}
+}
+
 func TestAddToMatchDB(t *testing.T) {
 	fixture := createDatabase(t)
 	defer fixture.TearDown()
@@ -77,9 +86,7 @@
 	got, err := fixture.db.ReturnMatches()
 	check(t, err, "Failed ReturnMatches()")
 
-	if !reflect.DeepEqual(correct, got) {
-		t.Fatalf("Got %#v,\nbut expected %#v.", got, correct)
-	}
+	checkDeepEqual(t, correct, got)
 }
 
 func TestAddOrUpdateRankingsDB(t *testing.T) {
@@ -198,6 +205,7 @@
 
 	stats := Stats{
 		TeamNumber: 1236, MatchNumber: 7,
+		SetNumber: 1, CompLevel: "qual",
 		StartingQuadrant: 2,
 		AutoBallPickedUp: [5]bool{false, false, false, true, false},
 		ShotsMissed:      9, UpperGoalShots: 5, LowerGoalShots: 4,
@@ -698,25 +706,20 @@
 	got, err := fixture.db.QueryRankings(125)
 	check(t, err, "Failed QueryRankings()")
 
-	if !reflect.DeepEqual(correct, got) {
-		t.Errorf("Got %#v,\nbut expected %#v.", got, correct)
-	}
+	checkDeepEqual(t, correct, got)
 }
 
 func TestNotes(t *testing.T) {
 	fixture := createDatabase(t)
 	defer fixture.TearDown()
 
-	expected := NotesData{
-		TeamNumber: 1234,
-		Notes:      []string{"Note 1", "Note 3"},
-	}
+	expected := []string{"Note 1", "Note 3"}
 
-	err := fixture.db.AddNotes(NotesData{1234, []string{"Note 1"}})
+	err := fixture.db.AddNotes(NotesData{TeamNumber: 1234, Notes: "Note 1", GoodDriving: true, BadDriving: false, SketchyClimb: false, SolidClimb: true, GoodDefense: false, BadDefense: true})
 	check(t, err, "Failed to add Note")
-	err = fixture.db.AddNotes(NotesData{1235, []string{"Note 2"}})
+	err = fixture.db.AddNotes(NotesData{TeamNumber: 1235, Notes: "Note 2", GoodDriving: false, BadDriving: true, SketchyClimb: false, SolidClimb: true, GoodDefense: false, BadDefense: false})
 	check(t, err, "Failed to add Note")
-	err = fixture.db.AddNotes(NotesData{1234, []string{"Note 3"}})
+	err = fixture.db.AddNotes(NotesData{TeamNumber: 1234, Notes: "Note 3", GoodDriving: true, BadDriving: false, SketchyClimb: false, SolidClimb: true, GoodDefense: true, BadDefense: false})
 	check(t, err, "Failed to add Note")
 
 	actual, err := fixture.db.QueryNotes(1234)
diff --git a/scouting/scouting_test.ts b/scouting/scouting_test.ts
index a34d98c..15a0509 100644
--- a/scouting/scouting_test.ts
+++ b/scouting/scouting_test.ts
@@ -241,7 +241,6 @@
 
     expect(await getHeadingText()).toEqual('Climb');
     await element(by.id('high')).click();
-    await setTextboxByIdTo('comment', 'A very useful comment here.');
     await element(by.buttonText('Next')).click();
 
     expect(await getHeadingText()).toEqual('Other');
@@ -249,6 +248,7 @@
     await adjustNthSliderBy(1, 1);
     await element(by.id('no_show')).click();
     await element(by.id('mechanically_broke')).click();
+    await setTextboxByIdTo('comment', 'A very useful comment here.');
     await element(by.buttonText('Next')).click();
 
     expect(await getHeadingText()).toEqual('Review and Submit');
@@ -273,7 +273,6 @@
 
     // Validate Climb.
     await expectReviewFieldToBe('Climb Level', 'High');
-    await expectReviewFieldToBe('Comments', 'A very useful comment here.');
 
     // Validate Other.
     await expectReviewFieldToBe('Defense Played On Rating', '3');
@@ -282,6 +281,7 @@
     await expectReviewFieldToBe('Never moved', 'false');
     await expectReviewFieldToBe('Battery died', 'false');
     await expectReviewFieldToBe('Broke (mechanically)', 'true');
+    await expectReviewFieldToBe('Comments', 'A very useful comment here.');
 
     await element(by.buttonText('Submit')).click();
     await browser.wait(
@@ -322,4 +322,36 @@
       await element(by.buttonText('Flip')).click();
     }
   });
+
+  it('should: submit note scouting for multiple teams', async () => {
+    // Navigate to Notes Page.
+    await loadPage();
+    await element(by.cssContainingText('.nav-link', 'Notes')).click();
+    expect(await element(by.id('page-title')).getText()).toEqual('Notes');
+
+    // Add first team.
+    await setTextboxByIdTo('team_number_notes', '1234');
+    await element(by.buttonText('Select')).click();
+
+    // Add note and select keyword for first team.
+    expect(await element(by.id('team-key-1')).getText()).toEqual('1234');
+    await element(by.id('text-input-1')).sendKeys('Good Driving');
+    await element(by.id('Good Driving_0')).click();
+
+    // Navigate to add team selection and add another team.
+    await element(by.id('add-team-button')).click();
+    await setTextboxByIdTo('team_number_notes', '1235');
+    await element(by.buttonText('Select')).click();
+
+    // Add note and select keyword for second team.
+    expect(await element(by.id('team-key-2')).getText()).toEqual('1235');
+    await element(by.id('text-input-2')).sendKeys('Bad Driving');
+    await element(by.id('Bad Driving_1')).click();
+
+    // Submit Notes.
+    await element(by.buttonText('Submit')).click();
+    expect(await element(by.id('team_number_label')).getText()).toEqual(
+      'Team Number'
+    );
+  });
 });
diff --git a/scouting/webserver/main.go b/scouting/webserver/main.go
index 5d4ab01..d2fbdfe 100644
--- a/scouting/webserver/main.go
+++ b/scouting/webserver/main.go
@@ -117,7 +117,7 @@
 	if err != nil {
 		log.Fatal("Failed to connect to database: ", err)
 	}
-	defer database.Close()
+	defer database.Delete()
 
 	scrapeMatchList := func(year int32, eventCode string) ([]scraping.Match, error) {
 		if *blueAllianceConfigPtr == "" {
diff --git a/scouting/webserver/requests/messages/submit_notes.fbs b/scouting/webserver/requests/messages/submit_notes.fbs
index cf111b3..1498e26 100644
--- a/scouting/webserver/requests/messages/submit_notes.fbs
+++ b/scouting/webserver/requests/messages/submit_notes.fbs
@@ -3,6 +3,12 @@
 table SubmitNotes {
     team:int (id: 0);
     notes:string (id: 1);
+    good_driving:bool (id: 2);
+    bad_driving:bool (id: 3);
+    sketchy_climb:bool (id: 4);
+    solid_climb:bool (id: 5);
+    good_defense:bool (id: 6);
+    bad_defense:bool (id: 7);
 }
 
 root_type SubmitNotes;
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 67a1722..e33e82d 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -66,7 +66,7 @@
 	QueryMatches(int32) ([]db.Match, error)
 	QueryAllShifts(int) ([]db.Shift, error)
 	QueryStats(int) ([]db.Stats, error)
-	QueryNotes(int32) (db.NotesData, error)
+	QueryNotes(int32) ([]string, error)
 	AddNotes(db.NotesData) error
 }
 
@@ -337,7 +337,33 @@
 func parseTeamKey(teamKey string) (int, error) {
 	// TBA prefixes teams with "frc". Not sure why. Get rid of that.
 	teamKey = strings.TrimPrefix(teamKey, "frc")
-	return strconv.Atoi(teamKey)
+	magnitude := 0
+	if strings.HasSuffix(teamKey, "A") {
+		magnitude = 0
+		teamKey = strings.TrimSuffix(teamKey, "A")
+	} else if strings.HasSuffix(teamKey, "B") {
+		magnitude = 9
+		teamKey = strings.TrimSuffix(teamKey, "B")
+	} else if strings.HasSuffix(teamKey, "C") {
+		magnitude = 8
+		teamKey = strings.TrimSuffix(teamKey, "C")
+	} else if strings.HasSuffix(teamKey, "D") {
+		magnitude = 7
+		teamKey = strings.TrimSuffix(teamKey, "D")
+	} else if strings.HasSuffix(teamKey, "E") {
+		magnitude = 6
+		teamKey = strings.TrimSuffix(teamKey, "E")
+	} else if strings.HasSuffix(teamKey, "F") {
+		magnitude = 5
+		teamKey = strings.TrimSuffix(teamKey, "F")
+	}
+
+	if magnitude != 0 {
+		teamKey = strconv.Itoa(magnitude) + teamKey
+	}
+
+	result, err := strconv.Atoi(teamKey)
+	return result, err
 }
 
 // Parses the alliance data from the specified match and returns the three red
@@ -445,8 +471,14 @@
 	}
 
 	err = handler.db.AddNotes(db.NotesData{
-		TeamNumber: request.Team(),
-		Notes:      []string{string(request.Notes())},
+		TeamNumber:   request.Team(),
+		Notes:        string(request.Notes()),
+		GoodDriving:  bool(request.GoodDriving()),
+		BadDriving:   bool(request.BadDriving()),
+		SketchyClimb: bool(request.SketchyClimb()),
+		SolidClimb:   bool(request.SolidClimb()),
+		GoodDefense:  bool(request.GoodDefense()),
+		BadDefense:   bool(request.BadDefense()),
 	})
 	if err != nil {
 		respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to insert notes: %v", err))
@@ -475,14 +507,14 @@
 		return
 	}
 
-	notesData, err := handler.db.QueryNotes(request.Team())
+	notes, err := handler.db.QueryNotes(request.Team())
 	if err != nil {
 		respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to query notes: %v", err))
 		return
 	}
 
 	var response RequestNotesForTeamResponseT
-	for _, data := range notesData.Notes {
+	for _, data := range notes {
 		response.Notes = append(response.Notes, &request_notes_for_team_response.NoteT{data})
 	}
 
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index 44fd5db..85ab916 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -314,8 +314,14 @@
 
 	builder := flatbuffers.NewBuilder(1024)
 	builder.Finish((&submit_notes.SubmitNotesT{
-		Team:  971,
-		Notes: "Notes",
+		Team:         971,
+		Notes:        "Notes",
+		GoodDriving:  true,
+		BadDriving:   false,
+		SketchyClimb: true,
+		SolidClimb:   false,
+		GoodDefense:  true,
+		BadDefense:   false,
 	}).Pack(builder))
 
 	_, err := debug.SubmitNotes("http://localhost:8080", builder.FinishedBytes())
@@ -324,7 +330,16 @@
 	}
 
 	expected := []db.NotesData{
-		{TeamNumber: 971, Notes: []string{"Notes"}},
+		{
+			TeamNumber:   971,
+			Notes:        "Notes",
+			GoodDriving:  true,
+			BadDriving:   false,
+			SketchyClimb: true,
+			SolidClimb:   false,
+			GoodDefense:  true,
+			BadDefense:   false,
+		},
 	}
 
 	if !reflect.DeepEqual(database.notes, expected) {
@@ -335,8 +350,14 @@
 func TestRequestNotes(t *testing.T) {
 	database := MockDatabase{
 		notes: []db.NotesData{{
-			TeamNumber: 971,
-			Notes:      []string{"Notes"},
+			TeamNumber:   971,
+			Notes:        "Notes",
+			GoodDriving:  true,
+			BadDriving:   false,
+			SketchyClimb: true,
+			SolidClimb:   false,
+			GoodDefense:  true,
+			BadDefense:   false,
 		}},
 	}
 	scoutingServer := server.NewScoutingServer()
@@ -584,14 +605,14 @@
 	return []db.Stats{}, nil
 }
 
-func (database *MockDatabase) QueryNotes(requestedTeam int32) (db.NotesData, error) {
+func (database *MockDatabase) QueryNotes(requestedTeam int32) ([]string, error) {
 	var results []string
 	for _, data := range database.notes {
 		if data.TeamNumber == requestedTeam {
-			results = append(results, data.Notes[0])
+			results = append(results, data.Notes)
 		}
 	}
-	return db.NotesData{TeamNumber: requestedTeam, Notes: results}, nil
+	return results, nil
 }
 
 func (database *MockDatabase) AddNotes(data db.NotesData) error {
diff --git a/scouting/www/entry/entry.component.css b/scouting/www/entry/entry.component.css
index 6d13657..a78a00a 100644
--- a/scouting/www/entry/entry.component.css
+++ b/scouting/www/entry/entry.component.css
@@ -8,8 +8,8 @@
 }
 
 textarea {
-  width: 300px;
-  height: 150px;
+  width: 350px;
+  height: 180px;
 }
 
 button {
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index 9be8db7..e73cfb0 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -254,10 +254,6 @@
       </label>
       <br />
     </form>
-    <div class="row">
-      <h4>Comments</h4>
-      <textarea [(ngModel)]="comment" id="comment"></textarea>
-    </div>
     <div class="buttons">
       <button class="btn btn-primary" (click)="prevSection()">Back</button>
       <button class="btn btn-primary" (click)="nextSection()">Next</button>
@@ -360,6 +356,15 @@
       </form>
     </div>
 
+    <div class="row">
+      <h4>General Comments About Match</h4>
+      <textarea
+        [(ngModel)]="comment"
+        id="comment"
+        placeholder="optional"
+      ></textarea>
+    </div>
+
     <div class="buttons">
       <button class="btn btn-primary" (click)="prevSection()">Back</button>
       <button class="btn btn-primary" (click)="nextSection()">Next</button>
@@ -398,7 +403,6 @@
     <h4>Climb</h4>
     <ul>
       <li>Climb Level: {{level | levelToString}}</li>
-      <li>Comments: {{comment}}</li>
     </ul>
 
     <h4>Other</h4>
@@ -410,6 +414,7 @@
       <li>Battery died: {{batteryDied}}</li>
       <li>Broke (mechanically): {{mechanicallyBroke}}</li>
       <li>Lost coms: {{lostComs}}</li>
+      <li>Comments: {{comment}}</li>
     </ul>
 
     <span class="error_message">{{ errorMessage }}</span>
diff --git a/scouting/www/notes/notes.component.css b/scouting/www/notes/notes.component.css
index 869bdab..67b2351 100644
--- a/scouting/www/notes/notes.component.css
+++ b/scouting/www/notes/notes.component.css
@@ -6,7 +6,6 @@
   width: calc(100% - 20px);
 }
 
-.buttons {
-  display: flex;
-  justify-content: space-between;
+.container-main {
+  padding-left: 20px;
 }
diff --git a/scouting/www/notes/notes.component.ts b/scouting/www/notes/notes.component.ts
index 0f0eb82..00faddd 100644
--- a/scouting/www/notes/notes.component.ts
+++ b/scouting/www/notes/notes.component.ts
@@ -1,4 +1,4 @@
-import {Component} from '@angular/core';
+import {Component, HostListener} from '@angular/core';
 import {Builder, ByteBuffer} from 'flatbuffers';
 import {ErrorResponse} from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
 import {RequestNotesForTeam} from 'org_frc971/scouting/webserver/requests/messages/request_notes_for_team_generated';
@@ -9,88 +9,169 @@
 import {SubmitNotes} from 'org_frc971/scouting/webserver/requests/messages/submit_notes_generated';
 import {SubmitNotesResponse} from 'org_frc971/scouting/webserver/requests/messages/submit_notes_response_generated';
 
+/*
+For new games, the keywords being used will likely need to be updated.
+To update the keywords complete the following: 
+  1) Update the Keywords Interface and KEYWORD_CHECKBOX_LABELS in notes.component.ts
+    The keys of Keywords and KEYWORD_CHECKBOX_LABELS should match.
+  2) In notes.component.ts, update the setTeamNumber() method with the new keywords.
+  3) Add/Edit the new keywords in /scouting/webserver/requests/messages/submit_notes.fbs.
+  4) In notes.component.ts, update the submitData() method with the newKeywords 
+    so that it matches the updated flatbuffer
+  5) In db.go, update the NotesData struct and the 
+    AddNotes method with the new keywords        
+  6) In db_test.go update the TestNotes method so the test uses the keywords
+  7) Update the submitNoteScoutingHandler in requests.go with the new keywords
+  8) Finally, update the corresponding test in requests_test.go (TestSubmitNotes)
+  
+  Note: If you change the number of keywords you might need to 
+    update how they are displayed in notes.ng.html 
+*/
+
+// TeamSelection: Display form to add a team to the teams being scouted.
+// Data: Display the note textbox and keyword selection form
+// for all the teams being scouted.
 type Section = 'TeamSelection' | 'Data';
 
-interface Note {
-  readonly data: string;
+// Every keyword checkbox corresponds to a boolean.
+// If the boolean is True, the checkbox is selected
+// and the note scout saw that the robot being scouted
+// displayed said property (ex. Driving really well -> goodDriving)
+interface Keywords {
+  goodDriving: boolean;
+  badDriving: boolean;
+  sketchyClimb: boolean;
+  solidClimb: boolean;
+  goodDefense: boolean;
+  badDefense: boolean;
 }
 
+interface Input {
+  teamNumber: number;
+  notesData: string;
+  keywordsData: Keywords;
+}
+
+const KEYWORD_CHECKBOX_LABELS = {
+  goodDriving: 'Good Driving',
+  badDriving: 'Bad Driving',
+  solidClimb: 'Solid Climb',
+  sketchyClimb: 'Sketchy Climb',
+  goodDefense: 'Good Defense',
+  badDefense: 'Bad Defense',
+} as const;
+
 @Component({
   selector: 'frc971-notes',
   templateUrl: './notes.ng.html',
   styleUrls: ['../common.css', './notes.component.css'],
 })
 export class Notes {
+  // Re-export KEYWORD_CHECKBOX_LABELS so that we can
+  // use it in the checkbox properties.
+  readonly KEYWORD_CHECKBOX_LABELS = KEYWORD_CHECKBOX_LABELS;
+
+  // Necessary in order to iterate the keys of KEYWORD_CHECKBOX_LABELS.
+  Object = Object;
+
   section: Section = 'TeamSelection';
-  notes: Note[] = [];
 
   errorMessage = '';
+  teamNumberSelection: number = 971;
 
-  teamNumber: number = 971;
-  newData = '';
+  // Data inputted by user is stored in this array.
+  // Includes the team number, notes, and keyword selection.
+  newData: Input[] = [];
 
-  async setTeamNumber() {
-    const builder = new Builder();
-    RequestNotesForTeam.startRequestNotesForTeam(builder);
-    RequestNotesForTeam.addTeam(builder, this.teamNumber);
-    builder.finish(RequestNotesForTeam.endRequestNotesForTeam(builder));
+  // Keyboard shortcuts to switch between text areas.
+  // Listens for Ctrl + number and focuses on the
+  // corresponding textbox.
+  // More Info: https://angular.io/api/core/HostListener
 
-    const buffer = builder.asUint8Array();
-    const res = await fetch('/requests/request/notes_for_team', {
-      method: 'POST',
-      body: buffer,
-    });
-
-    const resBuffer = await res.arrayBuffer();
-    const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
-
-    if (res.ok) {
-      this.notes = [];
-      const parsedResponse =
-        RequestNotesForTeamResponse.getRootAsRequestNotesForTeamResponse(
-          fbBuffer
-        );
-      for (let i = 0; i < parsedResponse.notesLength(); i++) {
-        const fbNote = parsedResponse.notes(i);
-        this.notes.push({data: fbNote.data()});
+  @HostListener('window:keyup', ['$event'])
+  onEvent(event: KeyboardEvent) {
+    if (event.ctrlKey) {
+      if (event.code.includes('Digit')) {
+        this.handleFocus(event.key);
       }
-      this.section = 'Data';
-    } else {
-      const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
-
-      const errorMessage = parsedResponse.errorMessage();
-      this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
     }
   }
 
-  changeTeam() {
+  handleFocus(digit: string) {
+    let textArea = <HTMLInputElement>(
+      document.getElementById('text-input-' + digit)
+    );
+    if (textArea != null) {
+      textArea.focus();
+    }
+  }
+
+  setTeamNumber() {
+    let data: Input = {
+      teamNumber: this.teamNumberSelection,
+      notesData: '',
+      keywordsData: {
+        goodDriving: false,
+        badDriving: false,
+        solidClimb: false,
+        sketchyClimb: false,
+        goodDefense: false,
+        badDefense: false,
+      },
+    };
+
+    this.newData.push(data);
+    this.section = 'Data';
+  }
+
+  removeTeam(index: number) {
+    this.newData.splice(index, 1);
+    if (this.newData.length == 0) {
+      this.section = 'TeamSelection';
+    } else {
+      this.section = 'Data';
+    }
+  }
+
+  addTeam() {
     this.section = 'TeamSelection';
   }
 
   async submitData() {
-    const builder = new Builder();
-    const dataFb = builder.createString(this.newData);
-    builder.finish(
-      SubmitNotes.createSubmitNotes(builder, this.teamNumber, dataFb)
-    );
+    for (let i = 0; i < this.newData.length; i++) {
+      const builder = new Builder();
+      const dataFb = builder.createString(this.newData[i].notesData);
+      builder.finish(
+        SubmitNotes.createSubmitNotes(
+          builder,
+          this.newData[i].teamNumber,
+          dataFb,
+          this.newData[i].keywordsData.goodDriving,
+          this.newData[i].keywordsData.badDriving,
+          this.newData[i].keywordsData.sketchyClimb,
+          this.newData[i].keywordsData.solidClimb,
+          this.newData[i].keywordsData.goodDefense,
+          this.newData[i].keywordsData.badDefense
+        )
+      );
 
-    const buffer = builder.asUint8Array();
-    const res = await fetch('/requests/submit/submit_notes', {
-      method: 'POST',
-      body: buffer,
-    });
+      const buffer = builder.asUint8Array();
+      const res = await fetch('/requests/submit/submit_notes', {
+        method: 'POST',
+        body: buffer,
+      });
 
-    if (res.ok) {
-      this.newData = '';
-      this.errorMessage = '';
-      await this.setTeamNumber();
-    } else {
-      const resBuffer = await res.arrayBuffer();
-      const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
-      const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
-
-      const errorMessage = parsedResponse.errorMessage();
-      this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+      if (!res.ok) {
+        const resBuffer = await res.arrayBuffer();
+        const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
+        const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
+        const errorMessage = parsedResponse.errorMessage();
+        this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
+      }
     }
+
+    this.newData = [];
+    this.errorMessage = '';
+    this.section = 'TeamSelection';
   }
 }
diff --git a/scouting/www/notes/notes.ng.html b/scouting/www/notes/notes.ng.html
index a69ba88..b51a7ce 100644
--- a/scouting/www/notes/notes.ng.html
+++ b/scouting/www/notes/notes.ng.html
@@ -1,10 +1,12 @@
-<h2>Notes</h2>
+<h2 id="page-title">Notes</h2>
 
 <ng-container [ngSwitch]="section">
   <div *ngSwitchCase="'TeamSelection'">
-    <label for="team_number_notes">Team Number</label>
+    <label id="team_number_label" class="label" for="team_number_notes">
+      Team Number
+    </label>
     <input
-      [(ngModel)]="teamNumber"
+      [(ngModel)]="teamNumberSelection"
       type="number"
       id="team_number_notes"
       min="1"
@@ -14,17 +16,107 @@
   </div>
 
   <div *ngSwitchCase="'Data'">
-    <h3>Scouting team: {{teamNumber}}</h3>
-    <ul *ngFor="let note of notes">
-      <li class="note">{{ note.data }}</li>
-    </ul>
-    <textarea class="text-input" [(ngModel)]="newData"></textarea>
-    <div class="buttons">
-      <button class="btn btn-primary" (click)="changeTeam()">
-        Change team
-      </button>
-      <button class="btn btn-primary" (click)="submitData()">Submit</button>
+    <div class="container-main" *ngFor="let team of newData; let i = index">
+      <div class="pt-2 pb-2">
+        <div class="d-flex flex-row">
+          <div>
+            <button
+              class="btn bg-transparent ml-10 md-5"
+              (click)="removeTeam(i)"
+            >
+              &#10006;
+              <!--X Symbol-->
+            </button>
+          </div>
+          <div><h3 id="team-key-{{i+1}}">{{team.teamNumber}}</h3></div>
+        </div>
+        <div class="">
+          <!--
+          Note Input Text Areas.
+          ID property is used for keyboard shorcuts to focus
+          on the corresponding text area.
+          The data-toggle and title properties are
+          used for bootstrap tooltips.
+          -->
+          <textarea
+            class="text-input"
+            id="text-input-{{i+1}}"
+            [(ngModel)]="newData[i].notesData"
+            data-toggle="tooltip"
+            title="Ctrl + {{i+1}}"
+          ></textarea>
+        </div>
+        <!--Key Word Checkboxes-->
+        <!--Row 1 (Prevent Overflow on mobile by splitting checkboxes into 2 rows)-->
+        <!--Slice KEYWORD_CHECKBOX_LABELS using https://angular.io/api/common/SlicePipe-->
+        <div class="d-flex flex-row justify-content-around">
+          <div
+            *ngFor="let key of Object.keys(KEYWORD_CHECKBOX_LABELS) | slice:0:((Object.keys(KEYWORD_CHECKBOX_LABELS).length)/2); let k = index"
+          >
+            <div class="form-check">
+              <input
+                class="form-check-input"
+                [(ngModel)]="newData[i]['keywordsData'][key]"
+                type="checkbox"
+                id="{{KEYWORD_CHECKBOX_LABELS[key]}}_{{i}}"
+                name="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+              />
+              <label
+                class="form-check-label"
+                for="{{KEYWORD_CHECKBOX_LABELS[key]}}_{{i}}"
+              >
+                {{KEYWORD_CHECKBOX_LABELS[key]}}
+              </label>
+              <br />
+            </div>
+          </div>
+        </div>
+        <!--Row 2 (Prevent Overflow on mobile by splitting checkboxes into 2 rows)-->
+        <div class="d-flex flex-row justify-content-around">
+          <div
+            *ngFor="let key of Object.keys(KEYWORD_CHECKBOX_LABELS) | slice:3:(Object.keys(KEYWORD_CHECKBOX_LABELS).length); let k = index"
+          >
+            <div class="form-check">
+              <input
+                class="form-check-input"
+                [(ngModel)]="newData[i]['keywordsData'][key]"
+                type="checkbox"
+                id="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+                name="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+              />
+              <label
+                class="form-check-label"
+                for="{{KEYWORD_CHECKBOX_LABELS[key]}}"
+              >
+                {{KEYWORD_CHECKBOX_LABELS[key]}}
+              </label>
+              <br />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="d-flex flex-row justify-content-center pt-2">
+      <div>
+        <button
+          id="add-team-button"
+          class="btn btn-secondary"
+          (click)="addTeam()"
+        >
+          Add team
+        </button>
+      </div>
+      <div>
+        <button
+          id="submit-button"
+          class="btn btn-success"
+          (click)="submitData()"
+        >
+          Submit
+        </button>
+      </div>
     </div>
   </div>
+
   <div class="error">{{errorMessage}}</div>
 </ng-container>
diff --git a/tools/go/go_mirrors.bzl b/tools/go/go_mirrors.bzl
index ee8b1b8..338cc95 100644
--- a/tools/go/go_mirrors.bzl
+++ b/tools/go/go_mirrors.bzl
@@ -1,11 +1,11 @@
 # This file is auto-generated. Do not edit.
 GO_MIRROR_INFO = {
     "co_honnef_go_tools": {
-        "filename": "co_honnef_go_tools__v0.0.0-20190523083050-ea95bdfd59fc.zip",
+        "filename": "co_honnef_go_tools__v0.0.1-2019.2.3.zip",
         "importpath": "honnef.co/go/tools",
-        "sha256": "eeaa82700e96ac5e803d7a9c32363332504beff8fbb1202492b4d43d5a5e7360",
-        "strip_prefix": "honnef.co/go/tools@v0.0.0-20190523083050-ea95bdfd59fc",
-        "version": "v0.0.0-20190523083050-ea95bdfd59fc",
+        "sha256": "539825114c487680f99df80f6107410e1e53bbfd5deb931b84d1faf2d221638e",
+        "strip_prefix": "honnef.co/go/tools@v0.0.1-2019.2.3",
+        "version": "v0.0.1-2019.2.3",
     },
     "com_github_antihax_optional": {
         "filename": "com_github_antihax_optional__v1.0.0.zip",
@@ -77,6 +77,20 @@
         "strip_prefix": "github.com/cockroachdb/apd@v1.1.0",
         "version": "v1.1.0",
     },
+    "com_github_coreos_go_systemd": {
+        "filename": "com_github_coreos_go_systemd__v0.0.0-20190719114852-fd7a80b32e1f.zip",
+        "importpath": "github.com/coreos/go-systemd",
+        "sha256": "22237f0aed3ab6018a1025c65f4f45b4c05f9aa0c0bb9ec880294273b9a15bf2",
+        "strip_prefix": "github.com/coreos/go-systemd@v0.0.0-20190719114852-fd7a80b32e1f",
+        "version": "v0.0.0-20190719114852-fd7a80b32e1f",
+    },
+    "com_github_creack_pty": {
+        "filename": "com_github_creack_pty__v1.1.7.zip",
+        "importpath": "github.com/creack/pty",
+        "sha256": "e7ea3403784d186aefbe84caed958f8cba2e72a04f30cdb291ece19bec39c8f3",
+        "strip_prefix": "github.com/creack/pty@v1.1.7",
+        "version": "v1.1.7",
+    },
     "com_github_davecgh_go_spew": {
         "filename": "com_github_davecgh_go_spew__v1.1.1.zip",
         "importpath": "github.com/davecgh/go-spew",
@@ -105,6 +119,27 @@
         "strip_prefix": "github.com/ghodss/yaml@v1.0.0",
         "version": "v1.0.0",
     },
+    "com_github_go_kit_log": {
+        "filename": "com_github_go_kit_log__v0.1.0.zip",
+        "importpath": "github.com/go-kit/log",
+        "sha256": "e0676df7357654a000008dfad3b6b211cba3595f32d3e220edd63a4c9d0d9254",
+        "strip_prefix": "github.com/go-kit/log@v0.1.0",
+        "version": "v0.1.0",
+    },
+    "com_github_go_logfmt_logfmt": {
+        "filename": "com_github_go_logfmt_logfmt__v0.5.0.zip",
+        "importpath": "github.com/go-logfmt/logfmt",
+        "sha256": "59a6b59ae3da84f7a58373844ca8d298f5007ce0e173437fc85c26d4fc76ca8b",
+        "strip_prefix": "github.com/go-logfmt/logfmt@v0.5.0",
+        "version": "v0.5.0",
+    },
+    "com_github_go_stack_stack": {
+        "filename": "com_github_go_stack_stack__v1.8.0.zip",
+        "importpath": "github.com/go-stack/stack",
+        "sha256": "78c2667c710f811307038634ffa43af442619acfeaf1efb593aa4e0ded9df48f",
+        "strip_prefix": "github.com/go-stack/stack@v1.8.0",
+        "version": "v1.8.0",
+    },
     "com_github_gofrs_uuid": {
         "filename": "com_github_gofrs_uuid__v4.0.0+incompatible.zip",
         "importpath": "github.com/gofrs/uuid",
@@ -147,6 +182,13 @@
         "strip_prefix": "github.com/google/go-querystring@v1.1.0",
         "version": "v1.1.0",
     },
+    "com_github_google_renameio": {
+        "filename": "com_github_google_renameio__v0.1.0.zip",
+        "importpath": "github.com/google/renameio",
+        "sha256": "b8510bb34078691a20b8e4902d371afe0eb171b2daf953f67cb3960d1926ccf3",
+        "strip_prefix": "github.com/google/renameio@v0.1.0",
+        "version": "v0.1.0",
+    },
     "com_github_google_uuid": {
         "filename": "com_github_google_uuid__v1.1.2.zip",
         "importpath": "github.com/google/uuid",
@@ -161,19 +203,138 @@
         "strip_prefix": "github.com/grpc-ecosystem/grpc-gateway@v1.16.0",
         "version": "v1.16.0",
     },
-    "com_github_jackc_fake": {
-        "filename": "com_github_jackc_fake__v0.0.0-20150926172116-812a484cc733.zip",
-        "importpath": "github.com/jackc/fake",
-        "sha256": "bf8b5b51ae03f572a70a0582dc663c5733bba9aca785d39bb0367797148e6d64",
-        "strip_prefix": "github.com/jackc/fake@v0.0.0-20150926172116-812a484cc733",
-        "version": "v0.0.0-20150926172116-812a484cc733",
+    "com_github_jackc_chunkreader": {
+        "filename": "com_github_jackc_chunkreader__v1.0.0.zip",
+        "importpath": "github.com/jackc/chunkreader",
+        "sha256": "e204c917e2652ffe047f5c8b031192757321f568654e3df8408bf04178df1408",
+        "strip_prefix": "github.com/jackc/chunkreader@v1.0.0",
+        "version": "v1.0.0",
     },
-    "com_github_jackc_pgx": {
-        "filename": "com_github_jackc_pgx__v3.6.2+incompatible.zip",
-        "importpath": "github.com/jackc/pgx",
-        "sha256": "73675895baa0da97b2f0ce6e895c69b7c77ad994e30ce6a1add2abc3bb17e375",
-        "strip_prefix": "github.com/jackc/pgx@v3.6.2+incompatible",
-        "version": "v3.6.2+incompatible",
+    "com_github_jackc_chunkreader_v2": {
+        "filename": "com_github_jackc_chunkreader_v2__v2.0.1.zip",
+        "importpath": "github.com/jackc/chunkreader/v2",
+        "sha256": "6e3f4b7d9647f31061f6446ae10de71fc1407e64f84cd0949afac0cd231e8dd2",
+        "strip_prefix": "github.com/jackc/chunkreader/v2@v2.0.1",
+        "version": "v2.0.1",
+    },
+    "com_github_jackc_pgconn": {
+        "filename": "com_github_jackc_pgconn__v1.12.1.zip",
+        "importpath": "github.com/jackc/pgconn",
+        "sha256": "48d34064a1facff7766713d9224502e7376a5d90c1506f99a37c57bfceaf9636",
+        "strip_prefix": "github.com/jackc/pgconn@v1.12.1",
+        "version": "v1.12.1",
+    },
+    "com_github_jackc_pgio": {
+        "filename": "com_github_jackc_pgio__v1.0.0.zip",
+        "importpath": "github.com/jackc/pgio",
+        "sha256": "1a83c03d53f6a40339364cafcbbabb44238203c79ca0c9b98bf582d0df0e0468",
+        "strip_prefix": "github.com/jackc/pgio@v1.0.0",
+        "version": "v1.0.0",
+    },
+    "com_github_jackc_pgmock": {
+        "filename": "com_github_jackc_pgmock__v0.0.0-20210724152146-4ad1a8207f65.zip",
+        "importpath": "github.com/jackc/pgmock",
+        "sha256": "0fffd0a7a67dbdfafa04297e51028c6d2d08cd6691f3b6d78d7ae6502d3d4cf2",
+        "strip_prefix": "github.com/jackc/pgmock@v0.0.0-20210724152146-4ad1a8207f65",
+        "version": "v0.0.0-20210724152146-4ad1a8207f65",
+    },
+    "com_github_jackc_pgpassfile": {
+        "filename": "com_github_jackc_pgpassfile__v1.0.0.zip",
+        "importpath": "github.com/jackc/pgpassfile",
+        "sha256": "1cc79fb0b80f54b568afd3f4648dd1c349f746ad7c379df8d7f9e0eb1cac938b",
+        "strip_prefix": "github.com/jackc/pgpassfile@v1.0.0",
+        "version": "v1.0.0",
+    },
+    "com_github_jackc_pgproto3": {
+        "filename": "com_github_jackc_pgproto3__v1.1.0.zip",
+        "importpath": "github.com/jackc/pgproto3",
+        "sha256": "e3766bee50ed74e49a067b2c4797a2c69015cf104bf3f3624cd483a9e940b4ee",
+        "strip_prefix": "github.com/jackc/pgproto3@v1.1.0",
+        "version": "v1.1.0",
+    },
+    "com_github_jackc_pgproto3_v2": {
+        "filename": "com_github_jackc_pgproto3_v2__v2.3.0.zip",
+        "importpath": "github.com/jackc/pgproto3/v2",
+        "sha256": "6b702c372e13520636243d3be58922968f0630b67e23ba77326ef6ee4cada463",
+        "strip_prefix": "github.com/jackc/pgproto3/v2@v2.3.0",
+        "version": "v2.3.0",
+    },
+    "com_github_jackc_pgservicefile": {
+        "filename": "com_github_jackc_pgservicefile__v0.0.0-20200714003250-2b9c44734f2b.zip",
+        "importpath": "github.com/jackc/pgservicefile",
+        "sha256": "8422a25b9d2b0be05c66ee1ccfdbaab144ce98f1ac678bc647064c560d4cd6e2",
+        "strip_prefix": "github.com/jackc/pgservicefile@v0.0.0-20200714003250-2b9c44734f2b",
+        "version": "v0.0.0-20200714003250-2b9c44734f2b",
+    },
+    "com_github_jackc_pgtype": {
+        "filename": "com_github_jackc_pgtype__v1.11.0.zip",
+        "importpath": "github.com/jackc/pgtype",
+        "sha256": "6a257b81c0bd386d6241219a14ebd41d574a02aeaeb3942670c06441b864dcad",
+        "strip_prefix": "github.com/jackc/pgtype@v1.11.0",
+        "version": "v1.11.0",
+    },
+    "com_github_jackc_pgx_v4": {
+        "filename": "com_github_jackc_pgx_v4__v4.16.1.zip",
+        "importpath": "github.com/jackc/pgx/v4",
+        "sha256": "c3a169a68ff0e56f9f81eee4de4d2fd2a5ec7f4d6be159159325f4863c80bd10",
+        "strip_prefix": "github.com/jackc/pgx/v4@v4.16.1",
+        "version": "v4.16.1",
+    },
+    "com_github_jackc_puddle": {
+        "filename": "com_github_jackc_puddle__v1.2.1.zip",
+        "importpath": "github.com/jackc/puddle",
+        "sha256": "40d73550686666eb1f6df02b65008b2a4c98cfed1254dc4866e6ebe95fbc5c95",
+        "strip_prefix": "github.com/jackc/puddle@v1.2.1",
+        "version": "v1.2.1",
+    },
+    "com_github_jinzhu_inflection": {
+        "filename": "com_github_jinzhu_inflection__v1.0.0.zip",
+        "importpath": "github.com/jinzhu/inflection",
+        "sha256": "cf1087a6f6653ed5f366f85cf0110bbbf581d4e9bc8a4d1a9b56765d94b546c3",
+        "strip_prefix": "github.com/jinzhu/inflection@v1.0.0",
+        "version": "v1.0.0",
+    },
+    "com_github_jinzhu_now": {
+        "filename": "com_github_jinzhu_now__v1.1.4.zip",
+        "importpath": "github.com/jinzhu/now",
+        "sha256": "245473b8e50be3897751ec66dd6be93588de261920e0345b500f692924575872",
+        "strip_prefix": "github.com/jinzhu/now@v1.1.4",
+        "version": "v1.1.4",
+    },
+    "com_github_kisielk_gotool": {
+        "filename": "com_github_kisielk_gotool__v1.0.0.zip",
+        "importpath": "github.com/kisielk/gotool",
+        "sha256": "089dbba6e3aa09944fdb40d72acc86694e8bdde01cfc0f40fe0248309eb80a3f",
+        "strip_prefix": "github.com/kisielk/gotool@v1.0.0",
+        "version": "v1.0.0",
+    },
+    "com_github_konsorten_go_windows_terminal_sequences": {
+        "filename": "com_github_konsorten_go_windows_terminal_sequences__v1.0.2.zip",
+        "importpath": "github.com/konsorten/go-windows-terminal-sequences",
+        "sha256": "4d00d71b8de60bcaf454f8f867210ebcd05e75c0a7c2725904f71aa2f20fb08e",
+        "strip_prefix": "github.com/konsorten/go-windows-terminal-sequences@v1.0.2",
+        "version": "v1.0.2",
+    },
+    "com_github_kr_pretty": {
+        "filename": "com_github_kr_pretty__v0.1.0.zip",
+        "importpath": "github.com/kr/pretty",
+        "sha256": "06063d21457e06dc2aba4a5bd09771147ec3d8ab40b224f26e55c5a76089ca43",
+        "strip_prefix": "github.com/kr/pretty@v0.1.0",
+        "version": "v0.1.0",
+    },
+    "com_github_kr_pty": {
+        "filename": "com_github_kr_pty__v1.1.8.zip",
+        "importpath": "github.com/kr/pty",
+        "sha256": "d66e6fbc65e772289a7ff8c58ab2cdfb886253053b0cea11ba3ca1738b2d6bc6",
+        "strip_prefix": "github.com/kr/pty@v1.1.8",
+        "version": "v1.1.8",
+    },
+    "com_github_kr_text": {
+        "filename": "com_github_kr_text__v0.1.0.zip",
+        "importpath": "github.com/kr/text",
+        "sha256": "9363a4c8f1f3387a36014de51b477b831a13981fc59a5665f9d21609bea9e77c",
+        "strip_prefix": "github.com/kr/text@v0.1.0",
+        "version": "v0.1.0",
     },
     "com_github_lib_pq": {
         "filename": "com_github_lib_pq__v1.10.2.zip",
@@ -182,6 +343,27 @@
         "strip_prefix": "github.com/lib/pq@v1.10.2",
         "version": "v1.10.2",
     },
+    "com_github_masterminds_semver_v3": {
+        "filename": "com_github_masterminds_semver_v3__v3.1.1.zip",
+        "importpath": "github.com/Masterminds/semver/v3",
+        "sha256": "0a46c7403dfeda09b0821e851f8e1cec8f1ea4276281e42ea399da5bc5bf0704",
+        "strip_prefix": "github.com/Masterminds/semver/v3@v3.1.1",
+        "version": "v3.1.1",
+    },
+    "com_github_mattn_go_colorable": {
+        "filename": "com_github_mattn_go_colorable__v0.1.6.zip",
+        "importpath": "github.com/mattn/go-colorable",
+        "sha256": "0da5d3779775f6fe5d007e7ec8e0afc136c4bd7b8c9b5cd73254db26773cf4dc",
+        "strip_prefix": "github.com/mattn/go-colorable@v0.1.6",
+        "version": "v0.1.6",
+    },
+    "com_github_mattn_go_isatty": {
+        "filename": "com_github_mattn_go_isatty__v0.0.12.zip",
+        "importpath": "github.com/mattn/go-isatty",
+        "sha256": "07941d24e0894c29dc42bcd29d644815cd7b5ee84e3c14bbe6d51ad13efcbf07",
+        "strip_prefix": "github.com/mattn/go-isatty@v0.0.12",
+        "version": "v0.0.12",
+    },
     "com_github_phst_runfiles": {
         "filename": "com_github_phst_runfiles__v0.0.0-20220125203201-388095b3a22d.zip",
         "importpath": "github.com/phst/runfiles",
@@ -217,6 +399,34 @@
         "strip_prefix": "github.com/rogpeppe/fastuuid@v1.2.0",
         "version": "v1.2.0",
     },
+    "com_github_rogpeppe_go_internal": {
+        "filename": "com_github_rogpeppe_go_internal__v1.3.0.zip",
+        "importpath": "github.com/rogpeppe/go-internal",
+        "sha256": "191b95c35d85a5683cee6e303a08b4d103bf9de9ececdc6904f21ed90c094b0a",
+        "strip_prefix": "github.com/rogpeppe/go-internal@v1.3.0",
+        "version": "v1.3.0",
+    },
+    "com_github_rs_xid": {
+        "filename": "com_github_rs_xid__v1.2.1.zip",
+        "importpath": "github.com/rs/xid",
+        "sha256": "4abdedc4de69adcb9a4575f99c59d8ab542191e1800b6a91e12a4e9ea8da0026",
+        "strip_prefix": "github.com/rs/xid@v1.2.1",
+        "version": "v1.2.1",
+    },
+    "com_github_rs_zerolog": {
+        "filename": "com_github_rs_zerolog__v1.15.0.zip",
+        "importpath": "github.com/rs/zerolog",
+        "sha256": "8e98c48e7fd132aafbf129664e8fd65229d067d772bff4bd712a497b7a2f00c4",
+        "strip_prefix": "github.com/rs/zerolog@v1.15.0",
+        "version": "v1.15.0",
+    },
+    "com_github_satori_go_uuid": {
+        "filename": "com_github_satori_go_uuid__v1.2.0.zip",
+        "importpath": "github.com/satori/go.uuid",
+        "sha256": "4f741306a0cbe97581e34a638531bcafe3c2848150539a2ec2ba12c5e3e6cbdd",
+        "strip_prefix": "github.com/satori/go.uuid@v1.2.0",
+        "version": "v1.2.0",
+    },
     "com_github_shopspring_decimal": {
         "filename": "com_github_shopspring_decimal__v1.2.0.zip",
         "importpath": "github.com/shopspring/decimal",
@@ -224,12 +434,19 @@
         "strip_prefix": "github.com/shopspring/decimal@v1.2.0",
         "version": "v1.2.0",
     },
+    "com_github_sirupsen_logrus": {
+        "filename": "com_github_sirupsen_logrus__v1.4.2.zip",
+        "importpath": "github.com/sirupsen/logrus",
+        "sha256": "9a8e55830261a4b1c9350d7c45db029c8586c0b2d934d1224cde469425031edd",
+        "strip_prefix": "github.com/sirupsen/logrus@v1.4.2",
+        "version": "v1.4.2",
+    },
     "com_github_stretchr_objx": {
-        "filename": "com_github_stretchr_objx__v0.1.0.zip",
+        "filename": "com_github_stretchr_objx__v0.2.0.zip",
         "importpath": "github.com/stretchr/objx",
-        "sha256": "1fa10dab404ed7fc8ed2a033f8784187d5df3513ced3841ce39e46d37850eb1d",
-        "strip_prefix": "github.com/stretchr/objx@v0.1.0",
-        "version": "v0.1.0",
+        "sha256": "5517d43cfb7e628b9c2c64010b934e346cd24726e3d6eaf02b7f86e10752e968",
+        "strip_prefix": "github.com/stretchr/objx@v0.2.0",
+        "version": "v0.2.0",
     },
     "com_github_stretchr_testify": {
         "filename": "com_github_stretchr_testify__v1.7.0.zip",
@@ -238,6 +455,13 @@
         "strip_prefix": "github.com/stretchr/testify@v1.7.0",
         "version": "v1.7.0",
     },
+    "com_github_zenazn_goji": {
+        "filename": "com_github_zenazn_goji__v0.9.0.zip",
+        "importpath": "github.com/zenazn/goji",
+        "sha256": "0807a255d9d715d18427a6eedd8e4f5a22670b09e5f45fddd229c1ae38da25a9",
+        "strip_prefix": "github.com/zenazn/goji@v0.9.0",
+        "version": "v0.9.0",
+    },
     "com_google_cloud_go": {
         "filename": "com_google_cloud_go__v0.34.0.zip",
         "importpath": "cloud.google.com/go",
@@ -246,11 +470,25 @@
         "version": "v0.34.0",
     },
     "in_gopkg_check_v1": {
-        "filename": "in_gopkg_check_v1__v0.0.0-20161208181325-20d25e280405.zip",
+        "filename": "in_gopkg_check_v1__v1.0.0-20180628173108-788fd7840127.zip",
         "importpath": "gopkg.in/check.v1",
-        "sha256": "4e1817f964ca34e545b81afda0325a5e89cf58de2e413d8207c0afddd0fdc15c",
-        "strip_prefix": "gopkg.in/check.v1@v0.0.0-20161208181325-20d25e280405",
-        "version": "v0.0.0-20161208181325-20d25e280405",
+        "sha256": "4bc535ed2aac48a231af8b6005a0b5f6069dadab9a3d65b1e9f1fe91c74d8e61",
+        "strip_prefix": "gopkg.in/check.v1@v1.0.0-20180628173108-788fd7840127",
+        "version": "v1.0.0-20180628173108-788fd7840127",
+    },
+    "in_gopkg_errgo_v2": {
+        "filename": "in_gopkg_errgo_v2__v2.1.0.zip",
+        "importpath": "gopkg.in/errgo.v2",
+        "sha256": "6b8954819a20ec52982a206fd3eb94629ff53c5790aa77534e6d8daf7de01bee",
+        "strip_prefix": "gopkg.in/errgo.v2@v2.1.0",
+        "version": "v2.1.0",
+    },
+    "in_gopkg_inconshreveable_log15_v2": {
+        "filename": "in_gopkg_inconshreveable_log15_v2__v2.0.0-20180818164646-67afb5ed74ec.zip",
+        "importpath": "gopkg.in/inconshreveable/log15.v2",
+        "sha256": "799307ed46ca30ca0ac2dc0332f3673814b8ff6cc1ee905a462ccfd438e8e695",
+        "strip_prefix": "gopkg.in/inconshreveable/log15.v2@v2.0.0-20180818164646-67afb5ed74ec",
+        "version": "v2.0.0-20180818164646-67afb5ed74ec",
     },
     "in_gopkg_yaml_v2": {
         "filename": "in_gopkg_yaml_v2__v2.2.3.zip",
@@ -266,6 +504,20 @@
         "strip_prefix": "gopkg.in/yaml.v3@v3.0.0-20200313102051-9f266ea9e77c",
         "version": "v3.0.0-20200313102051-9f266ea9e77c",
     },
+    "io_gorm_driver_postgres": {
+        "filename": "io_gorm_driver_postgres__v1.3.7.zip",
+        "importpath": "gorm.io/driver/postgres",
+        "sha256": "b38fed3060ea8ee200d50666a9c6230f2c387d4ab930b70dd859b93f5fac7771",
+        "strip_prefix": "gorm.io/driver/postgres@v1.3.7",
+        "version": "v1.3.7",
+    },
+    "io_gorm_gorm": {
+        "filename": "io_gorm_gorm__v1.23.5.zip",
+        "importpath": "gorm.io/gorm",
+        "sha256": "34219a6d2ac9b9c340f811e5863a98b150db6d1fd5b8f02777299863c1628e0f",
+        "strip_prefix": "gorm.io/gorm@v1.23.5",
+        "version": "v1.23.5",
+    },
     "io_opentelemetry_go_proto_otlp": {
         "filename": "io_opentelemetry_go_proto_otlp__v0.7.0.zip",
         "importpath": "go.opentelemetry.io/proto/otlp",
@@ -302,11 +554,11 @@
         "version": "v1.26.0",
     },
     "org_golang_x_crypto": {
-        "filename": "org_golang_x_crypto__v0.0.0-20210711020723-a769d52b0f97.zip",
+        "filename": "org_golang_x_crypto__v0.0.0-20210921155107-089bfa567519.zip",
         "importpath": "golang.org/x/crypto",
-        "sha256": "b2b28fcf49bf385183f0369851145ddd93989f68d9e675db536a3dd482ca6d76",
-        "strip_prefix": "golang.org/x/crypto@v0.0.0-20210711020723-a769d52b0f97",
-        "version": "v0.0.0-20210711020723-a769d52b0f97",
+        "sha256": "eb2426a7891915213cc5da1da7b6fc6e9e2cf253d518d8e169e038e287f414e3",
+        "strip_prefix": "golang.org/x/crypto@v0.0.0-20210921155107-089bfa567519",
+        "version": "v0.0.0-20210921155107-089bfa567519",
     },
     "org_golang_x_exp": {
         "filename": "org_golang_x_exp__v0.0.0-20190121172915-509febef88a4.zip",
@@ -316,11 +568,18 @@
         "version": "v0.0.0-20190121172915-509febef88a4",
     },
     "org_golang_x_lint": {
-        "filename": "org_golang_x_lint__v0.0.0-20190313153728-d0100b6bd8b3.zip",
+        "filename": "org_golang_x_lint__v0.0.0-20190930215403-16217165b5de.zip",
         "importpath": "golang.org/x/lint",
-        "sha256": "5c7bb9792bdc4ec4cf1af525cf9998f8a958daf6495852c9a7dbb71738f2f10a",
-        "strip_prefix": "golang.org/x/lint@v0.0.0-20190313153728-d0100b6bd8b3",
-        "version": "v0.0.0-20190313153728-d0100b6bd8b3",
+        "sha256": "91323fe1a77f13de722a0ce8efc5c5f2da4f26216d858acec64cb23c956fa163",
+        "strip_prefix": "golang.org/x/lint@v0.0.0-20190930215403-16217165b5de",
+        "version": "v0.0.0-20190930215403-16217165b5de",
+    },
+    "org_golang_x_mod": {
+        "filename": "org_golang_x_mod__v0.1.1-0.20191105210325-c90efee705ee.zip",
+        "importpath": "golang.org/x/mod",
+        "sha256": "b1e6cb975c69d29974b4f77fd8a0f2f7e916a1fa971bab60fdd45ffe80a29f32",
+        "strip_prefix": "golang.org/x/mod@v0.1.1-0.20191105210325-c90efee705ee",
+        "version": "v0.1.1-0.20191105210325-c90efee705ee",
     },
     "org_golang_x_net": {
         "filename": "org_golang_x_net__v0.0.0-20210226172049-e18ecbb05110.zip",
@@ -358,18 +617,18 @@
         "version": "v0.0.0-20201126162022-7de9c90e9dd1",
     },
     "org_golang_x_text": {
-        "filename": "org_golang_x_text__v0.3.6.zip",
+        "filename": "org_golang_x_text__v0.3.7.zip",
         "importpath": "golang.org/x/text",
-        "sha256": "2afade648a4cb240afb7b3bf8e3719b615169c90d6281bd6d4ba34629c744579",
-        "strip_prefix": "golang.org/x/text@v0.3.6",
-        "version": "v0.3.6",
+        "sha256": "e1a9115e61a38da8bdc893d0ba83b65f89cc1114f152a98eb572c5ea6551e8d4",
+        "strip_prefix": "golang.org/x/text@v0.3.7",
+        "version": "v0.3.7",
     },
     "org_golang_x_tools": {
-        "filename": "org_golang_x_tools__v0.0.0-20190524140312-2c0ae7006135.zip",
+        "filename": "org_golang_x_tools__v0.0.0-20200103221440-774c71fcf114.zip",
         "importpath": "golang.org/x/tools",
-        "sha256": "86687e8cd5adccf8809ba031e59146d0c89047b6267aacc785ffc20b0ce6b735",
-        "strip_prefix": "golang.org/x/tools@v0.0.0-20190524140312-2c0ae7006135",
-        "version": "v0.0.0-20190524140312-2c0ae7006135",
+        "sha256": "1c26b6b98d945255dfb6112d71135de3919350250e44e552a7089f724d0b7bfc",
+        "strip_prefix": "golang.org/x/tools@v0.0.0-20200103221440-774c71fcf114",
+        "version": "v0.0.0-20200103221440-774c71fcf114",
     },
     "org_golang_x_xerrors": {
         "filename": "org_golang_x_xerrors__v0.0.0-20200804184101-5ec99f83aff1.zip",
@@ -378,4 +637,32 @@
         "strip_prefix": "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1",
         "version": "v0.0.0-20200804184101-5ec99f83aff1",
     },
+    "org_uber_go_atomic": {
+        "filename": "org_uber_go_atomic__v1.6.0.zip",
+        "importpath": "go.uber.org/atomic",
+        "sha256": "c5e1e9f48017d7c7f7bb4532235e33242a1508bee68abe3cd301b68fe8ecd552",
+        "strip_prefix": "go.uber.org/atomic@v1.6.0",
+        "version": "v1.6.0",
+    },
+    "org_uber_go_multierr": {
+        "filename": "org_uber_go_multierr__v1.5.0.zip",
+        "importpath": "go.uber.org/multierr",
+        "sha256": "64053b7f6129cf2588f9b9ef1e934a26a0381da0002add973ec99f1294c1fc1e",
+        "strip_prefix": "go.uber.org/multierr@v1.5.0",
+        "version": "v1.5.0",
+    },
+    "org_uber_go_tools": {
+        "filename": "org_uber_go_tools__v0.0.0-20190618225709-2cfd321de3ee.zip",
+        "importpath": "go.uber.org/tools",
+        "sha256": "988dba9c5074080240d33d98e8ce511532f728698db7a9a4ac316c02c94030d6",
+        "strip_prefix": "go.uber.org/tools@v0.0.0-20190618225709-2cfd321de3ee",
+        "version": "v0.0.0-20190618225709-2cfd321de3ee",
+    },
+    "org_uber_go_zap": {
+        "filename": "org_uber_go_zap__v1.13.0.zip",
+        "importpath": "go.uber.org/zap",
+        "sha256": "4b4d15be7b4ce8029ab7c90f2fcb4c98e655172ebaa5cdbe234401081000fa26",
+        "strip_prefix": "go.uber.org/zap@v1.13.0",
+        "version": "v1.13.0",
+    },
 }
diff --git a/y2022/BUILD b/y2022/BUILD
index 5d29f04..4a351a5 100644
--- a/y2022/BUILD
+++ b/y2022/BUILD
@@ -47,6 +47,7 @@
     ],
     data = [
         ":aos_config",
+        ":message_bridge_client.sh",
         "//y2022/image_streamer:image_streamer_start",
     ],
     dirs = [
diff --git a/y2022/constants.cc b/y2022/constants.cc
index c97058e..444e87d 100644
--- a/y2022/constants.cc
+++ b/y2022/constants.cc
@@ -131,23 +131,23 @@
 
   // Interpolation table for comp and practice robots
   r.shot_interpolation_table = InterpolationTable<Values::ShotParams>({
-      {1.0, {0.05, 19.4}},
-      {1.6, {0.05, 19.4}},
-      {1.9, {0.1, 19.4}},
-      {2.12, {0.13, 19.4}},
-      {2.9, {0.24, 19.9}},
+      {1.0, {0.12, 19.4}},
+      {1.6, {0.12, 19.4}},
+      {1.9, {0.17, 19.4}},
+      {2.12, {0.21, 19.4}},
+      {2.9, {0.30, 19.9}},
 
-      {3.2, {0.26, 20.7}},
+      {3.2, {0.33, 20.1}},
 
-      {3.60, {0.33, 20.9}},
-      {4.50, {0.38, 22.5}},
-      {4.9, {0.4, 22.9}},
-      {5.4, {0.4, 23.9}},
+      {3.60, {0.39, 20.65}},
+      {4.50, {0.44, 22.3}},
+      {4.9, {0.43, 22.75}}, // up to here
+      {5.4, {0.43, 23.85}},
 
-      {6.0, {0.40, 25.4}},
-      {7.0, {0.37, 28.1}},
+      {6.0, {0.42, 25.3}},
+      {7.0, {0.40, 27.7}},
 
-      {10.0, {0.37, 28.1}},
+      {10.0, {0.40, 27.7}},
   });
 
   if (false) {
@@ -238,13 +238,22 @@
           0.0634440443622909 + 0.213601224728352 + 0.0657973101027296 -
           0.114726411377978 - 0.980314029089968 - 0.0266013159299456 +
           0.0631240002215899 + 0.222882504808653 + 0.0370686419434252 -
-          0.0965027214840068 - 0.126737479717192;
+          0.0965027214840068 - 0.126737479717192 - 0.0773753775457 +
+          2.8132444751306;
       turret->subsystem_params.zeroing_constants.measured_absolute_position =
-          1.3081068967929;
+          1.16683731504739;
 
       flipper_arm_left->potentiometer_offset = -6.4;
       flipper_arm_right->potentiometer_offset = 5.56;
 
+      *turret_range = ::frc971::constants::Range{
+          .lower_hard = -7.0,  // Back Hard
+          .upper_hard = 3.4,   // Front Hard
+          .lower = -6.4,       // Back Soft
+          .upper = 2.9         // Front Soft
+      };
+      turret_params->range = *turret_range;
+
       catapult_params->zeroing_constants.measured_absolute_position =
           1.71723370408082;
       catapult->potentiometer_offset = -2.03383240293769;
@@ -256,6 +265,8 @@
       break;
 
     case kPracticeTeamNumber:
+      catapult_params->range.lower = -0.885;
+
       r.shot_interpolation_table = InterpolationTable<Values::ShotParams>({
           {1.0, {0.08, 20.0}},
           {1.6, {0.08, 20.0}},
@@ -276,7 +287,8 @@
           {10.0, {0.39, 28.25}},
       });
 
-      climber->potentiometer_offset = -0.1209073362519 + 0.0760598;
+      climber->potentiometer_offset =
+          -0.1209073362519 + 0.0760598 - 0.0221716219244 - 0.00321684;
       intake_front->potentiometer_offset = 3.06604378582351 - 0.60745632979918;
       intake_front->subsystem_params.zeroing_constants
           .measured_absolute_position = 0.143667561169188;
@@ -291,14 +303,18 @@
           0.0718028442723373 - 0.0793332946417493 + 0.233707527214682 +
           0.0828349540635251 + 0.677740533247017 - 0.0828349540635251 -
           0.0903654044329345 - 0.105426305171759 - 0.150609007388226 -
-          0.0338870266623506 - 0.0677740533247011;
+          0.0338870266623506 - 0.0677740533247011 - 0.135548106649404 - 0.6852;
       turret->subsystem_params.zeroing_constants.measured_absolute_position =
-          1.50798193457968;
-      turret_range->upper = 2.9;
-      turret_range->lower = -6.4;
+          0.8306;
+      *turret_range = ::frc971::constants::Range{
+          .lower_hard = -7.0,  // Back Hard
+          .upper_hard = 3.4,   // Front Hard
+          .lower = -6.4,       // Back Soft
+          .upper = 2.9         // Front Soft
+      };
       turret_params->range = *turret_range;
-      flipper_arm_left->potentiometer_offset = -4.39536583413615;
-      flipper_arm_right->potentiometer_offset = 4.36264091401229;
+      flipper_arm_left->potentiometer_offset = -4.39536583413615 - 0.108401297910291;
+      flipper_arm_right->potentiometer_offset = 4.36264091401229 + 0.175896445665755;
 
       catapult_params->zeroing_constants.measured_absolute_position =
           1.62909518684227;
diff --git a/y2022/message_bridge_client.sh b/y2022/message_bridge_client.sh
index c81076a..733905e 100755
--- a/y2022/message_bridge_client.sh
+++ b/y2022/message_bridge_client.sh
@@ -5,7 +5,37 @@
   ping -c 1 pi1 -W 1 && break;
   sleep 1
 done
+while true;
+do
+  ping -c 1 pi2 -W 1 && break;
+  sleep 1
+done
+while true;
+do
+  ping -c 1 pi3 -W 1 && break;
+  sleep 1
+done
+while true;
+do
+  ping -c 1 pi4 -W 1 && break;
+  sleep 1
+done
+while true;
+do
+  ping -c 1 pi5 -W 1 && break;
+  sleep 1
+done
+while true;
+do
+  ping -c 1 pi6 -W 1 && break;
+  sleep 1
+done
+while true;
+do
+  ping -c 1 roborio -W 1 && break;
+  sleep 1
+done
 
 echo Pinged
 
-exec /home/admin/bin/message_bridge_client "$@"
+exec message_bridge_client "$@"
diff --git a/y2022/vision/camera_definition.py b/y2022/vision/camera_definition.py
index d3e44b7..3b34ca2 100644
--- a/y2022/vision/camera_definition.py
+++ b/y2022/vision/camera_definition.py
@@ -109,24 +109,24 @@
             camera_yaw = 0.0
             T = np.array([-9.5 * 0.0254, -3.5 * 0.0254, 34.5 * 0.0254])
         elif pi_number == "pi3":
-            camera_yaw = 179.0 * np.pi / 180.0
+            camera_yaw = 182.0 * np.pi / 180.0
             T = np.array([-9.5 * 0.0254, 3.5 * 0.0254, 34.5 * 0.0254])
         elif pi_number == "pi4":
             camera_yaw = -90.0 * np.pi / 180.0
             T = np.array([-10.25 * 0.0254, -5.0 * 0.0254, 27.5 * 0.0254])
     elif team_number == 9971:
         if pi_number == "pi1":
-            camera_yaw = 180.5 * np.pi / 180.0
+            camera_yaw = 179.0 * np.pi / 180.0
             T = np.array([-9.5 * 0.0254, 3.25 * 0.0254, 35.5 * 0.0254])
         elif pi_number == "pi2":
             camera_yaw = 0.0
             T = np.array([-9.0 * 0.0254, -3.25 * 0.0254, 35.5 * 0.0254])
         elif pi_number == "pi3":
             camera_yaw = 90.0 * np.pi / 180.0
-            T = np.array([-10.5 * 0.0254, -5.0 * 0.0254, 29.5 * 0.0254])
+            T = np.array([-10.5 * 0.0254, 5.0 * 0.0254, 29.5 * 0.0254])
         elif pi_number == "pi4":
             camera_yaw = -90.0 * np.pi / 180.0
-            T = np.array([-10.5 * 0.0254, 5.0 * 0.0254, 28.0 * 0.0254])
+            T = np.array([-10.5 * 0.0254, -5.0 * 0.0254, 28.5 * 0.0254])
     else:
         glog.fatal("Unknown team number for extrinsics")
 
diff --git a/y2022/y2022_imu.json b/y2022/y2022_imu.json
index 817f051..bd2b326 100644
--- a/y2022/y2022_imu.json
+++ b/y2022/y2022_imu.json
@@ -367,7 +367,7 @@
   "applications": [
     {
       "name": "message_bridge_client",
-      "executable_name": "message_bridge_client",
+      "executable_name": "message_bridge_client.sh",
       "nodes": [
         "imu"
       ]
diff --git a/y2022/y2022_logger.json b/y2022/y2022_logger.json
index a8a4bbd..4024bf6 100644
--- a/y2022/y2022_logger.json
+++ b/y2022/y2022_logger.json
@@ -502,7 +502,7 @@
   "applications": [
     {
       "name": "logger_message_bridge_client",
-      "executable_name": "message_bridge_client",
+      "executable_name": "message_bridge_client.sh",
       "args": ["--rmem=8388608", "--rt_priority=16"],
       "nodes": [
         "logger"
diff --git a/y2022/y2022_pi_template.json b/y2022/y2022_pi_template.json
index a6b3f4a..99b04a1 100644
--- a/y2022/y2022_pi_template.json
+++ b/y2022/y2022_pi_template.json
@@ -186,7 +186,7 @@
       "name": "/pi{{ NUM }}/camera",
       "type": "y2022.vision.TargetEstimate",
       "source_node": "pi{{ NUM }}",
-      "frequency": 40,
+      "frequency": 80,
       "num_senders": 2,
       "max_size": 40000,
       "logger": "LOCAL_AND_REMOTE_LOGGER",
@@ -358,7 +358,7 @@
   "applications": [
     {
       "name": "message_bridge_client",
-      "executable_name": "message_bridge_client",
+      "executable_name": "message_bridge_client.sh",
       "args": ["--rt_priority=16"],
       "nodes": [
         "pi{{ NUM }}"