diff --git a/backend/app/controllers/post_versions_controller.rb b/backend/app/controllers/post_versions_controller.rb index 0c6f632..545b8e0 100644 --- a/backend/app/controllers/post_versions_controller.rb +++ b/backend/app/controllers/post_versions_controller.rb @@ -49,10 +49,10 @@ class PostVersionsController < ApplicationController rows.map do |row| cur_tags = - Array(normalise_json(row.tags_json)) + normalise_json(row.tags_json) .map { Post.tag_snapshot_literal(_1) } prev_tags = - Array(normalise_json(row.attributes['prev_tags_json'])) + (normalise_json(row.attributes['prev_tags_json']) || []) .map { Post.tag_snapshot_literal(_1) } { post_id: row.post_id, diff --git a/backend/db/migrate/20260727123600_add_tags_json_to_post_versions.rb b/backend/db/migrate/20260727123600_add_tags_json_to_post_versions.rb index d306715..7bbf3c8 100644 --- a/backend/db/migrate/20260727123600_add_tags_json_to_post_versions.rb +++ b/backend/db/migrate/20260727123600_add_tags_json_to_post_versions.rb @@ -30,19 +30,46 @@ class AddTagsJsonToPostVersions < ActiveRecord::Migration[8.0] intervals_by_name = build_intervals_by_name say_with_time 'Backfilling post_versions.tags_json' do - MigrationPostVersion - .where(tags_json: nil) - .find_each(batch_size: 500) do |version| - version.update_columns( - tags_json: build_tags_json(version, intervals_by_name)) + MigrationPostVersion.where(tags_json: nil).find_each(batch_size: 500) do |version| + version.update_columns(tags_json: build_tags_json(version, intervals_by_name)) end end - assert_backfill_complete! change_column_null :post_versions, :tags_json, false + + schema = connection.quote(JSON.generate({ + type: 'array', + items: { type: 'object', + properties: { id: { type: 'integer', minimum: 1 }, + version_no: { type: 'integer', minimum: 1 }, + name: { type: 'string', minLength: 1 }, + category: { type: 'string', enum: ['deerjikist', + 'meme', + 'character', + 'general', + 'material', + 'meta', + 'nico'] }, + sections: { type: 'array', + items: { type: 'object', + properties: { begin_ms: { type: 'integer', + minimum: 0 }, + end_ms: { type: ['integer', + 'null'], + minimum: 0 } }, + required: ['begin_ms', 'end_ms'], + additionalProperties: false } } }, + required: ['id', 'version_no', 'name', 'category', 'sections'], + additionalProperties: false } })) + + add_check_constraint :post_versions, + "JSON_SCHEMA_VALID(#{ schema }, tags_json)", + name: 'chk_post_versions_tags_json_schema' end def down + remove_check_constraint :post_versions, name: 'chk_post_versions_tags_json_schema' + remove_column :post_versions, :tags_json end @@ -269,11 +296,4 @@ class AddTagsJsonToPostVersions < ActiveRecord::Migration[8.0] raise "Duplicate tag IDs: #{ details }" end - - def assert_backfill_complete! - missing_count = MigrationPostVersion.where(tags_json: nil).count - return if missing_count.zero? - - raise "#{ missing_count } post versions were not backfilled" - end end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index d4a82c7..5ebbcc5 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -363,6 +363,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_27_123600) do t.check_constraint "(`video_ms` is null) or (`video_ms` > 0)", name: "chk_post_versions_video_ms_positive" t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "post_versions_event_type_valid" t.check_constraint "`version_no` > 0", name: "post_versions_version_no_positive" + t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\",\"minimum\":1},\"version_no\":{\"type\":\"integer\",\"minimum\":1},\"name\":{\"type\":\"string\",\"minLength\":1},\"category\":{\"type\":\"string\",\"enum\":[\"deerjikist\",\"meme\",\"character\",\"general\",\"material\",\"meta\",\"nico\"]},\"sections\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"begin_ms\":{\"type\":\"integer\",\"minimum\":0},\"end_ms\":{\"type\":[\"integer\",\"null\"],\"minimum\":0}},\"required\":[\"begin_ms\",\"end_ms\"],\"additionalProperties\":false}}},\"required\":[\"id\",\"version_no\",\"name\",\"category\",\"sections\"],\"additionalProperties\":false}}',`tags_json`)", name: "chk_post_versions_tags_json_schema" end create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|