diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 956538c3d..86332fdb5 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -11,10 +11,3 @@ require: # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. - -# Offense count: 2 -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/UniqueValidationWithoutIndex: - Exclude: - - 'app/models/article.rb' diff --git a/db/migrate/20200911140318_add_unique_index_to_articles_user_id_title_body_markdown.rb b/db/migrate/20200911140318_add_unique_index_to_articles_user_id_title_body_markdown.rb new file mode 100644 index 000000000..e4b928f0b --- /dev/null +++ b/db/migrate/20200911140318_add_unique_index_to_articles_user_id_title_body_markdown.rb @@ -0,0 +1,26 @@ +class AddUniqueIndexToArticlesUserIdTitleBodyMarkdown < ActiveRecord::Migration[6.0] + disable_ddl_transaction! + + def up + # to avoid the error "Values larger than 1/3 of a buffer page cannot be indexed." + # due to the fact that large text column cannot be indexed by btree indexes + # we are going to need to build an index on the hash of the `body_markdown` column. + # We also cannot use a `HASH` index as it's not supported by unique columns. + # I don't recommend using GiN or GiST as well + # See , + # and + # + # NOTE: using SQL as I couldn't find a way to have Rails generate it correctly + ActiveRecord::Base.connection.execute( + <<~SQL + CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "index_articles_on_user_id_and_title_and_digest_body_markdown" + ON "articles" + USING btree ("user_id", "title", digest("body_markdown", 'sha512'::text)); + SQL + ) + end + + def down + remove_index :articles, name: :index_articles_on_user_id_and_title_and_digest_body_markdown, algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index adc1103cc..63a98dfcb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_09_10_205316) do +ActiveRecord::Schema.define(version: 2020_09_11_140318) do # These are extensions that must be enabled in order to support this database enable_extension "citext" @@ -153,6 +153,7 @@ ActiveRecord::Schema.define(version: 2020_09_10_205316) do t.string "video_source_url" t.string "video_state" t.string "video_thumbnail_url" + t.index "user_id, title, digest(body_markdown, 'sha512'::text)", name: "index_articles_on_user_id_and_title_and_digest_body_markdown", unique: true t.index ["boost_states"], name: "index_articles_on_boost_states", using: :gin t.index ["canonical_url"], name: "index_articles_on_canonical_url", unique: true t.index ["comment_score"], name: "index_articles_on_comment_score" diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index 073c6473d..9eff3a8f2 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -43,6 +43,15 @@ RSpec.describe Article, type: :model do it { is_expected.not_to allow_value("foo").for(:main_image_background_hex_color) } + describe "#body_markdown" do + it "is unique scoped for user_id and title" do + art2 = build(:article, body_markdown: article.body_markdown, user: article.user, title: article.title) + + expect(art2).not_to be_valid + expect(art2.errors.full_messages.to_sentence).to match("markdown has already been taken") + end + end + describe "#after_commit" do it "on update enqueues job to index article to elasticsearch" do article.save