From 0b9fa03f6d82ee728c18a4e39e4a0183d14c7eac Mon Sep 17 00:00:00 2001 From: miteruzo Date: Thu, 1 Oct 2026 22:56:15 +0900 Subject: [PATCH] #55 --- .../migrate/20260922010000_create_scripts.rb | 5 ++- .../migrate/20260922013000_create_regions.rb | 25 +++++++++++ .../migrate/20260922020000_create_locales.rb | 17 ++++++- ...20260922030000_add_columns_to_tag_names.rb | 9 +++- ...0260922040000_add_names_to_tag_versions.rb | 44 +++++++++++++++++-- backend/db/schema.rb | 21 ++++++++- 6 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 backend/db/migrate/20260922013000_create_regions.rb diff --git a/backend/db/migrate/20260922010000_create_scripts.rb b/backend/db/migrate/20260922010000_create_scripts.rb index 1ebb5ac..b825c84 100644 --- a/backend/db/migrate/20260922010000_create_scripts.rb +++ b/backend/db/migrate/20260922010000_create_scripts.rb @@ -4,13 +4,16 @@ class CreateScripts < ActiveRecord::Migration[8.0] t.string :name, null: false t.datetime :deprecated_at, index: true t.datetime :created_at, null: false + + t.check_constraint "REGEXP_LIKE(code, '^[A-Z][a-z]{3}$', 'c')", + name: 'scripts_code_check' end execute <<~SQL INSERT INTO scripts(code, name, created_at) VALUES - ('Jpan', '漢字および仮名文字', #{ connection.quote Time.current }) + ('Jpan', '漢字、仮名文字およびラテン文字', #{ connection.quote Time.current }) SQL end diff --git a/backend/db/migrate/20260922013000_create_regions.rb b/backend/db/migrate/20260922013000_create_regions.rb new file mode 100644 index 0000000..c44c069 --- /dev/null +++ b/backend/db/migrate/20260922013000_create_regions.rb @@ -0,0 +1,25 @@ +class CreateRegions < ActiveRecord::Migration[8.0] + def up + create_table :regions, id: { type: :integer, limit: 2 }, primary_key: :code do |t| + t.column :alpha_code, 'CHAR(2)' + t.string :name, null: false + t.datetime :deprecated_at, index: true + t.datetime :created_at, null: false + + t.check_constraint "REGEXP_LIKE(alpha_code, '^[A-Z]{2}$', 'c')", + name: 'regions_alpha_code_check' + t.index [:alpha_code], unique: true + end + + execute <<~SQL + INSERT INTO + regions(code, alpha_code, name, created_at) + VALUES + (392, 'JP', '日本国', #{ connection.quote Time.current }) + SQL + end + + def down + drop_table :regions + end +end diff --git a/backend/db/migrate/20260922020000_create_locales.rb b/backend/db/migrate/20260922020000_create_locales.rb index afcd475..c396b69 100644 --- a/backend/db/migrate/20260922020000_create_locales.rb +++ b/backend/db/migrate/20260922020000_create_locales.rb @@ -3,19 +3,32 @@ class CreateLocales < ActiveRecord::Migration[8.0] create_table :locales, id: { type: :string, limit: 32 }, primary_key: :code do |t| t.string :language_code, limit: 16, null: false, index: true t.column :script_code, 'CHAR(4)', null: false + t.integer :region_code, limit: 2, null: false + t.string :variants, limit: 255, null: false, default: '' t.string :name, null: false t.datetime :deprecated_at, index: true t.datetime :created_at, null: false t.foreign_key :languages, column: :language_code, primary_key: :code t.foreign_key :scripts, column: :script_code, primary_key: :code + t.foreign_key :regions, column: :region_code, primary_key: :code + t.check_constraint ('REGEXP_LIKE(variants, ' + + "'^(" + + '(([a-z0-9]{5,8}|[0-9][a-z0-9]{3})' + + '(-([a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*' + + '(-x(-[a-z0-9]{1,8})+)?)' + + '|(x(-[a-z0-9]{1,8})+)' + + ")?$', 'c')"), + name: 'locales_variants_check' + + t.index [:language_code, :script_code, :region_code, :variants], unique: true end execute <<~SQL INSERT INTO - locales(code, language_code, script_code, name, created_at) + locales(code, language_code, script_code, region_code, name, created_at) VALUES - ('ja', 'ja', 'Jpan', '日本語', #{ connection.quote Time.current }) + ('ja', 'ja', 'Jpan', 392, '日本語', #{ connection.quote Time.current }) SQL end diff --git a/backend/db/migrate/20260922030000_add_columns_to_tag_names.rb b/backend/db/migrate/20260922030000_add_columns_to_tag_names.rb index 6f203ca..0924d82 100644 --- a/backend/db/migrate/20260922030000_add_columns_to_tag_names.rb +++ b/backend/db/migrate/20260922030000_add_columns_to_tag_names.rb @@ -8,7 +8,7 @@ class AddColumnsToTagNames < ActiveRecord::Migration[8.0] add_column :tag_names, :primary_flg, :boolean, null: false, after: :script_code, default: true add_column :tag_names, :auto_generated, :boolean, - null: false, default: true, after: :primary_flg + null: false, default: false, after: :primary_flg add_foreign_key :tag_names, :languages, column: :language_code, primary_key: :code add_foreign_key :tag_names, :scripts, column: :script_code, primary_key: :code @@ -41,9 +41,16 @@ class AddColumnsToTagNames < ActiveRecord::Migration[8.0] add_column :tag_names, :primary_tag_id, :bigint, as: 'CASE WHEN primary_flg THEN tag_id ELSE NULL END' add_index :tag_names, [:primary_tag_id, :language_code], unique: true + + add_check_constraint :tag_names, + 'primary_flg OR (NOT auto_generated)', + name: 'chk_tag_names_auto_generated_primary' end def down + remove_check_constraint :tag_names, + name: 'chk_tag_names_auto_generated_primary' + remove_index :tag_names, [:primary_tag_id, :language_code] remove_column :tag_names, :primary_tag_id diff --git a/backend/db/migrate/20260922040000_add_names_to_tag_versions.rb b/backend/db/migrate/20260922040000_add_names_to_tag_versions.rb index af7e5a7..817b6d4 100644 --- a/backend/db/migrate/20260922040000_add_names_to_tag_versions.rb +++ b/backend/db/migrate/20260922040000_add_names_to_tag_versions.rb @@ -10,10 +10,7 @@ class AddNamesToTagVersions < ActiveRecord::Migration[8.0] say_with_time 'Backfilling multilingual tag version names' do TagVersion.find_each do |version| aliases = version.aliases.to_s.split.filter_map do |name| - name.present? && - { name:, - script_code: 'Jpan', - auto_generated: false } + name.present? && { name:, script_code: 'Jpan' } end names = { ja: { name: version.name, script_code: 'Jpan', @@ -23,10 +20,49 @@ class AddNamesToTagVersions < ActiveRecord::Migration[8.0] end end + add_names_constraint! + change_column_null :tag_versions, :names, false end def down + remove_check_constraint :tag_versions, + name: 'chk_tag_versions_names_schema' + remove_column :tag_versions, :names end + + private + + def add_names_constraint! + schema = { type: 'object', + minProperties: 1, + patternProperties: { '^.{1,16}$': name_schema }, + additionalProperties: false } + + quoted_schema = connection.quote(JSON.generate(schema)) + + add_check_constraint :tag_versions, + "JSON_SCHEMA_VALID(#{ quoted_schema }, names)", + name: 'chk_tag_versions_names_schema' + end + + def name_schema + { type: 'object', + required: ['name', 'script_code', 'auto_generated', 'aliases'], + properties: { **name_schema_base, + auto_generated: { type: 'boolean' }, + aliases: { type: 'array', + uniqueItems: true, + items: { type: 'object', + required: ['name', 'script_code'], + properties: name_schema_base, + additionalProperties: false } } }, + additionalProperties: false } + end + + def name_schema_base + { name: { type: 'string', minLength: 1, maxLength: 255 }, + script_code: { type: 'string', pattern: '^[A-Z][a-z]{3}$' } } + end end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index 238aac4..56b70a0 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -148,12 +148,17 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do create_table "locales", primary_key: "code", id: { type: :string, limit: 32 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "language_code", limit: 16, null: false t.string "script_code", limit: 4, null: false + t.integer "region_code", limit: 2, null: false + t.string "variants", default: "", null: false t.string "name", null: false t.datetime "deprecated_at" t.datetime "created_at", null: false t.index ["deprecated_at"], name: "index_locales_on_deprecated_at" + t.index ["language_code", "script_code", "region_code", "variants"], name: "idx_on_language_code_script_code_region_code_varian_b0ba85c39a", unique: true t.index ["language_code"], name: "index_locales_on_language_code" + t.index ["region_code"], name: "fk_rails_175159992c" t.index ["script_code"], name: "fk_rails_0b74ce96a8" + t.check_constraint "regexp_like(`variants`,_utf8mb4'^(([a-z0-9]{5,8}|[0-9][a-z0-9]{3})(-([a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*)?$',_utf8mb4'c')", name: "locales_variants_check" end create_table "material_export_items", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -406,11 +411,22 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do t.check_constraint "`version_no` > 0", name: "chk_posts_version_no_positive" end + create_table "regions", primary_key: "code", id: { type: :integer, limit: 2 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.string "alpha_code", limit: 2 + t.string "name", null: false + t.datetime "deprecated_at" + t.datetime "created_at", null: false + t.index ["alpha_code"], name: "index_regions_on_alpha_code", unique: true + t.index ["deprecated_at"], name: "index_regions_on_deprecated_at" + t.check_constraint "regexp_like(`alpha_code`,_utf8mb4'^[A-Z]{2}$',_utf8mb4'c')", name: "regions_alpha_code_check" + end + create_table "scripts", primary_key: "code", id: { type: :string, limit: 4 }, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name", null: false t.datetime "deprecated_at" t.datetime "created_at", null: false t.index ["deprecated_at"], name: "index_scripts_on_deprecated_at" + t.check_constraint "regexp_like(`code`,_utf8mb4'^[A-Z][a-z]{3}$',_utf8mb4'c')", name: "scripts_code_check" end create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -450,7 +466,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do t.string "name", null: false t.string "script_code", limit: 4, null: false t.boolean "primary_flg", null: false - t.boolean "auto_generated", default: true, null: false + t.boolean "auto_generated", default: false, null: false t.bigint "canonical_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -460,6 +476,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do t.index ["primary_tag_id", "language_code"], name: "index_tag_names_on_primary_tag_id_and_language_code", unique: true t.index ["script_code"], name: "fk_rails_dd783b3d1c" t.index ["tag_id"], name: "index_tag_names_on_tag_id" + t.check_constraint "(0 <> `primary_flg`) or (0 = `auto_generated`)", name: "chk_tag_names_auto_generated_primary" end create_table "tag_similarities", primary_key: ["tag_id", "target_tag_id"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -487,6 +504,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc } t.index ["tag_id", "version_no"], name: "index_tag_versions_on_tag_id_and_version_no", unique: true t.check_constraint "`version_no` > 0", name: "tag_versions_version_no_positive" + t.check_constraint "json_schema_valid(_utf8mb4'{\"type\":\"object\",\"minProperties\":1,\"patternProperties\":{\"^.{1,16}$\":{\"type\":\"object\",\"required\":[\"name\",\"script_code\",\"auto_generated\",\"aliases\"],\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":255},\"script_code\":{\"type\":\"string\",\"pattern\":\"^[A-Z][a-z]{3}$\"},\"auto_generated\":{\"type\":\"boolean\"},\"aliases\":{\"type\":\"array\",\"uniqueItems\":true,\"items\":{\"type\":\"object\",\"required\":[\"name\",\"script_code\"],\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":255},\"script_code\":{\"type\":\"string\",\"pattern\":\"^[A-Z][a-z]{3}$\"}},\"additionalProperties\":false}}},\"additionalProperties\":false}},\"additionalProperties\":false}',`names`)", name: "chk_tag_versions_names_schema" end create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -737,6 +755,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do add_foreign_key "gekanator_questions", "gekanator_question_suggestions" add_foreign_key "gekanator_questions", "users", column: "created_by_id" add_foreign_key "locales", "languages", column: "language_code", primary_key: "code" + add_foreign_key "locales", "regions", column: "region_code", primary_key: "code" add_foreign_key "locales", "scripts", column: "script_code", primary_key: "code" add_foreign_key "material_export_items", "materials" add_foreign_key "material_export_items", "users", column: "created_by_user_id"