brendon / ranked-model

An acts_as_sortable/acts_as_list replacement built for Rails 4+
https://github.com/mixonic/ranked-model
MIT License
1.09k stars 134 forks source link

Creating records with ActiveRecord::Enum in rails 6.0 causes ActiveRecord::RecordNotUnique. #185

Closed harashoo closed 2 years ago

harashoo commented 2 years ago

It turns out that creating a record using the ActiveRecord::Enum scope in rails causes a ActiveRecord::RecordNotUnique. This error occurs in rails 6.0, not in rails 6.1.

It may not be caused by ranked-model, but I will report it.

The following is a reproduction of the error. My development environment is rails 6.0.4.6.

# db/schema.rb
ActiveRecord::Schema.define(version: 2022_02_10_045213) do
  create_table "products", force: :cascade do |t|
    t.string "name"
    t.integer "display_order"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.integer "product_type"
    t.index ["display_order"], name: "index_products_on_display_order", unique: true
  end
end
# models/product.rb
class Product < ApplicationRecord
  include RankedModel
  ranks :display_order

  enum product_type: { tv: 0, pc: 1 }
end
# rails console
irb(main):001:0> Product.count
   (0.8ms)  SELECT sqlite_version(*)
   (0.2ms)  SELECT COUNT(*) FROM "products"
=> 0
irb(main):002:0> Product.tv.create!(name: 'TV_1', display_order_position: 0)
DEPRECATION WARNING: Class level methods will no longer inherit scoping from `create!` in Rails 6.1. To continue using the scoped relation, pass it into the block directly. To instead access the full set of models, as Rails 6.1 will, use `Product.default_scoped`. (called from irb_binding at (irb):2)
   (0.1ms)  begin transaction
  Product Load (0.2ms)  SELECT "products"."id", "products"."display_order" FROM "products" WHERE "products"."product_type" = ? ORDER BY "products"."display_order" ASC LIMIT ?  [["product_type", 0], ["LIMIT", 1]]
  Product Exists? (0.1ms)  SELECT 1 AS one FROM "products" WHERE "products"."product_type" = ? AND "products"."display_order" = ? LIMIT ?  [["product_type", 0], ["display_order", 0], ["LIMIT", 1]]
  Product Create (0.4ms)  INSERT INTO "products" ("name", "display_order", "created_at", "updated_at", "product_type") VALUES (?, ?, ?, ?, ?)  [["name", "TV_1"], ["display_order", 0], ["created_at", "2022-02-20 04:14:08.447403"], ["updated_at", "2022-02-20 04:14:08.447403"], ["product_type", 0]]
   (2.1ms)  commit transaction
=> #<Product id: 1, name: "TV_1", display_order: 0, created_at: "2022-02-20 04:14:08", updated_at: "2022-02-20 04:14:08", product_type: "tv">
irb(main):003:0> Product.pc.create!(name: 'PC_1', display_order_position: 0)
DEPRECATION WARNING: Class level methods will no longer inherit scoping from `create!` in Rails 6.1. To continue using the scoped relation, pass it into the block directly. To instead access the full set of models, as Rails 6.1 will, use `Product.default_scoped`. (called from irb_binding at (irb):3)
   (0.1ms)  begin transaction
  Product Load (0.1ms)  SELECT "products"."id", "products"."display_order" FROM "products" WHERE "products"."product_type" = ? ORDER BY "products"."display_order" ASC LIMIT ?  [["product_type", 1], ["LIMIT", 1]]
  Product Exists? (0.1ms)  SELECT 1 AS one FROM "products" WHERE "products"."product_type" = ? AND "products"."display_order" = ? LIMIT ?  [["product_type", 1], ["display_order", 0], ["LIMIT", 1]]
  Product Create (0.6ms)  INSERT INTO "products" ("name", "display_order", "created_at", "updated_at", "product_type") VALUES (?, ?, ?, ?, ?)  [["name", "PC_1"], ["display_order", 0], ["created_at", "2022-02-20 04:14:39.575440"], ["updated_at", "2022-02-20 04:14:39.575440"], ["product_type", 1]]
   (0.1ms)  rollback transaction
Traceback (most recent call last):
        1: from (irb):3
ActiveRecord::RecordNotUnique (SQLite3::ConstraintException: UNIQUE constraint failed: products.display_order)

I can avoid the error if I don't use scope.

irb(main):010:0> Product.create!(name: 'PC_1', display_order_position: 0, product_type: :pc)
   (0.3ms)  begin transaction
  Product Load (0.3ms)  SELECT "products"."id", "products"."display_order" FROM "products" ORDER BY "products"."display_order" ASC LIMIT ?  [["LIMIT", 1]]
  Product Exists? (0.1ms)  SELECT 1 AS one FROM "products" WHERE "products"."display_order" = ? LIMIT ?  [["display_order", -1073741824], ["LIMIT", 1]]
  Product Create (1.1ms)  INSERT INTO "products" ("name", "display_order", "created_at", "updated_at", "product_type") VALUES (?, ?, ?, ?, ?)  [["name", "PC_1"], ["display_order", -1073741824], ["created_at", "2022-02-20 04:20:18.969229"], ["updated_at", "2022-02-20 04:20:18.969229"], ["product_type", 1]]
   (0.9ms)  commit transaction
=> #<Product id: 2, name: "PC_1", display_order: -1073741824, created_at: "2022-02-20 04:20:18", updated_at: "2022-02-20 04:20:18", product_type: "pc">
brendon commented 2 years ago

Hi @harashoo, I think you'd want to tell ranked-model about the scope:

ranks :display_order, with_same: :product_type   

Does that help? :)

harashoo commented 2 years ago

In my case, I wanted to determine the display_order, regardless of product_type.

brendon commented 2 years ago

Yes, I guess the issue is that your scope is limiting what ranked-model can find in terms of other list items. You're not allowing it access to the full list. There could be a case for ranked-model to unscope first and then only scope by the with_same condition.

I would suggest that you use your second method of passing in the product_type as an attribute as the most expedient option here.

If you'd like to play around with a PR for introducing unscoping to the finder, I'd be happy to take a look at your work.

harashoo commented 2 years ago

I would suggest that you use your second method of passing in the product_type as an attribute as the most expedient option here.

Yes, I think so too. :)

brendon commented 2 years ago

Very good. I'll close this for now, but feel free to provide a PR in the future if you find the need :)