diff --git a/server/src/migrations/2020-07-19-223917_update_item_stats/down.sql b/server/src/migrations/2020-07-19-223917_update_item_stats/down.sql
new file mode 100644
index 0000000000..291a97c5ce
--- /dev/null
+++ b/server/src/migrations/2020-07-19-223917_update_item_stats/down.sql
@@ -0,0 +1 @@
+-- This file should undo anything in `up.sql`
\ No newline at end of file
diff --git a/server/src/migrations/2020-07-19-223917_update_item_stats/up.sql b/server/src/migrations/2020-07-19-223917_update_item_stats/up.sql
new file mode 100644
index 0000000000..a747646b1d
--- /dev/null
+++ b/server/src/migrations/2020-07-19-223917_update_item_stats/up.sql
@@ -0,0 +1,39 @@
+-- This migration updates all "stats" fields for each armour item in player inventory.
+UPDATE
+    inventory
+SET
+    items = json_replace(
+        -- Replace inventory slots.
+        items,
+        '$.slots',
+        (
+            -- Replace each item in the inventory, by splitting the json into an array, applying our changes,
+            -- and then re-aggregating.
+            --
+            -- NOTE: SQLite does not seem to provide a way to guarantee the order is the same after aggregation!
+            -- For now, it *does* seem to order by slots.key, but this doesn't seem to be guaranteed by anything.
+            -- For explicitness, we still include the ORDER BY, even though it seems to have no effect.
+            SELECT json_group_array(
+                json_replace(
+                    slots.value,
+                    '$.kind.Armor.stats',
+                    CASE
+                    -- ONLY replace item stats when the stats field currently lacks "protection"
+                    -- (NOTE: This will also return true if the value is null, so if you are creating a nullable
+                    -- JSON field please be careful before rerunning this migration!).
+                    WHEN json_extract(slots.value, '$.kind.Armor.stats.protection') IS NULL
+                    THEN
+                        -- Replace armor stats with new armor
+                        json('{ "protection": { "Normal": 1.0 } }')
+                    ELSE
+                        -- The protection stat was already added.
+                        json_extract(slots.value, '$.kind.Armor.stats')
+                    END
+                )
+            )
+            -- Extract all item slots
+            FROM json_each(json_extract(items, '$.slots')) AS slots
+            ORDER BY slots.key
+        )
+    )
+;
diff --git a/server/src/persistence/mod.rs b/server/src/persistence/mod.rs
index 34c1d4377a..441d741c63 100644
--- a/server/src/persistence/mod.rs
+++ b/server/src/persistence/mod.rs
@@ -22,6 +22,9 @@ use tracing::warn;
 // See: https://docs.rs/diesel_migrations/1.4.0/diesel_migrations/macro.embed_migrations.html
 // This macro is called at build-time, and produces the necessary migration info
 // for the `embedded_migrations` call below.
+//
+// NOTE: Adding a useless comment to trigger the migrations being run.  Delete
+// when needed.
 embed_migrations!();
 
 /// Runs any pending database migrations. This is executed during server startup