Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

insert_at respects unique not null check (>= 0) db constraints #246

Merged
merged 33 commits into from
Jan 23, 2017
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6d7ddae
insert_at respects unique not null check (>= 0) constraints
zharikovpro Dec 22, 2016
567f867
killed orphaned function store_at_0 (was invented earlier by someone …
zharikovpro Dec 22, 2016
8cd67ca
wip: tests
zharikovpro Dec 23, 2016
17d5ed2
green tests
zharikovpro Dec 23, 2016
9aeaa2d
one red test
zharikovpro Dec 23, 2016
dac0b19
green tests for sequential increments
zharikovpro Dec 23, 2016
dbac159
SQLite and PostgreSQL tests green
zharikovpro Dec 23, 2016
a89b21f
better test
zharikovpro Dec 23, 2016
fa4c678
green tests for SQLite and PostgreSQL for DefaultScopedNotNullUniqueP…
zharikovpro Dec 23, 2016
1f5aedc
work in progress: some appraisals fixes
zharikovpro Dec 23, 2016
51bd155
Revert "work in progress: some appraisals fixes"
zharikovpro Dec 23, 2016
f529ad0
tests: add_index compatible with rails 3-2
zharikovpro Dec 23, 2016
0e2ed61
tests: rails 3-2 doesnt have .ids method for AR
zharikovpro Dec 23, 2016
49f5df6
Rakefile restored (corrupted by accident, sorry)
zharikovpro Dec 23, 2016
dbf1807
DefaultScopedNotNullUniquePositiveConstraintsTest compatible with Rai…
zharikovpro Dec 23, 2016
83cdf79
better test index name
zharikovpro Dec 23, 2016
674aa88
code review for shuffle_positions_on_intermediate_items - no unscope,…
zharikovpro Dec 29, 2016
3392191
sequential_update option for shuffle_positions_on_intermediate_items
zharikovpro Dec 29, 2016
58e81c2
sequential_updates configuration option description
zharikovpro Dec 29, 2016
0dc8572
wip: green tests for sequential_updates defaults, red NoAdditionMixin…
zharikovpro Dec 29, 2016
516053b
green tests with SequentialUpdatesDefault mixin and SequentialUpdates…
zharikovpro Dec 29, 2016
8036261
sequential_updates default value detection shouldn't throw exception …
zharikovpro Dec 29, 2016
2ddb04c
file rename
zharikovpro Jan 10, 2017
c7d238b
renamed var
zharikovpro Jan 10, 2017
1093911
made shuffle_positions_on_intermediate_items private again
zharikovpro Jan 10, 2017
e4e0802
green local tests for SequentialUpdatesDefiner with instance level la…
zharikovpro Jan 19, 2017
49c24b5
SequentialUpdatesDefiner readability refactoring
zharikovpro Jan 19, 2017
56fd879
Merge with origin repo's master
zharikovpro Jan 20, 2017
88d9af1
removed redundant require
zharikovpro Jan 20, 2017
5eb3e6e
review code style tweak
zharikovpro Jan 22, 2017
ec8f582
code shortened after review
zharikovpro Jan 22, 2017
27e688a
ActiveRecord::Acts::List::SequentialUpdatesMethodDefiner - proper cla…
zharikovpro Jan 22, 2017
1d458aa
use latest stable bundler version which do not cause failed Travis bu…
zharikovpro Jan 23, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 32 additions & 15 deletions lib/acts_as_list/active_record/acts/list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -317,32 +317,53 @@ def increment_positions_on_all_items
# Reorders intermediate items to support moving an item from old_position to new_position.
def shuffle_positions_on_intermediate_items(old_position, new_position, avoid_id = nil)
return if old_position == new_position
scope = acts_as_list_list
scope = acts_as_list_list.select(:id)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you can remove the pluck then you can remove this too I think.

Copy link
Contributor Author

@zharikovpro zharikovpro Dec 29, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, pluck will be removed, it's redundant. Still, wouldnt it be performance-wise to only select ids for repositioning? Otherwise DB response will be a little bit larger and slower.


if avoid_id
scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.class.connection.quote(avoid_id))
end

# unique constraint prevents regular increment_all and forces to do increments one by one
# http://stackoverflow.com/questions/7703196/sqlite-increment-unique-integer-field
# both SQLite and PostgreSQL (and most probably MySQL too) has same issue
update_one_by_one = acts_as_list_list.connection.index_exists?(acts_as_list_list.table_name, position_column, unique: true)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking this should be defined more globally (even though it's only used here) as it feels more like a configuration option. What do you think? You could set it at initialisation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first I thought that if you have unique index this is not an option, really, but a requirement. Still someone may want to disable this for performance reasons iif DB has deferred unique check on this table, and this will work too. Preparing update to make :sequential_updates option.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this option could be used in other methods in future, definitely the way to go, thanks for the feedback.


if old_position < new_position
# Decrement position of intermediate items
#
# e.g., if moving an item from 2 to 5,
# move [3, 4, 5] to [2, 3, 4]
scope.where(
items = scope.where(
"#{quoted_position_column_with_table_name} > ?", old_position
).where(
"#{quoted_position_column_with_table_name} <= ?", new_position
).decrement_all
)

if update_one_by_one
items.order("#{quoted_position_column_with_table_name} ASC").pluck(:id).each do |id|
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you plucking the id's only to re-find the records in order to decrement etc...? You could call .reload instead?

acts_as_list_list.find(id).decrement!(position_column)
end
else
items.decrement_all
end
else
# Increment position of intermediate items
#
# e.g., if moving an item from 5 to 2,
# move [2, 3, 4] to [3, 4, 5]
scope.where(
items = scope.where(
"#{quoted_position_column_with_table_name} >= ?", new_position
).where(
"#{quoted_position_column_with_table_name} < ?", old_position
).increment_all
)

if update_one_by_one
items.order("#{quoted_position_column_with_table_name} DESC").pluck(:id).each do |id|
acts_as_list_list.find(id).increment!(position_column)
end
else
items.increment_all
end
end
end

Expand All @@ -352,23 +373,19 @@ def insert_at_position(position)
if in_list?
old_position = send(position_column).to_i
return if position == old_position
shuffle_positions_on_intermediate_items(old_position, position)
# temporary move after bottom with gap, avoiding duplicate values
# gap is required to leave room for position increments
# positive number will be valid with unique not null check (>= 0) db constraint
tmp_position_after_everything = acts_as_list_class.unscoped.maximum(position_column).to_i + 2
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this unscoping remove the acts_as_list scope? I assume you're not wanting to the position column to be globally unique? just unique per scope?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you're not wanting to the position column to be globally unique?

I want exactly this to bypass unique index on table column constraint. Still, you're right that it's not needed. There could be no such situation when different scopes have same positions in one table with unique index.

set_list_position(tmp_position_after_everything)
shuffle_positions_on_intermediate_items(old_position, position, id)
else
increment_positions_on_lower_items(position)
end
set_list_position(position)
end
end

# used by insert_at_position instead of remove_from_list, as postgresql raises error if position_column has non-null constraint
def store_at_0
if in_list?
old_position = send(position_column).to_i
set_list_position(0)
decrement_positions_on_lower_items(old_position)
end
end

def update_positions
old_position = send("#{position_column}_was") || bottom_position_in_list + 1
new_position = send(position_column).to_i
Expand Down
40 changes: 39 additions & 1 deletion test/test_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
ActiveRecord::Schema.verbose = false

def setup_db(position_options = {})
# sqlite cannot drop/rename/alter columns and add constraints after table creation
sqlite = ENV.fetch("DB", "sqlite") == "sqlite"

# AR caches columns options like defaults etc. Clear them!
ActiveRecord::Base.connection.create_table :mixins do |t|
t.column :pos, :integer, position_options
t.column :pos, :integer, position_options unless position_options[:positive] && sqlite
t.column :active, :boolean, default: true
t.column :parent_id, :integer
t.column :parent_type, :string
Expand All @@ -17,6 +20,20 @@ def setup_db(position_options = {})
t.column :state, :integer
end

if position_options[:unique] && !(sqlite && position_options[:positive])
ActiveRecord::Base.connection.add_index :mixins, :pos, unique: true
end

if position_options[:positive]
if sqlite
# SQLite cannot add constraint after table creation, also cannot add unique inside ADD COLUMN
ActiveRecord::Base.connection.execute('ALTER TABLE mixins ADD COLUMN pos integer8 NOT NULL CHECK (pos > 0) DEFAULT 1')
ActiveRecord::Base.connection.execute('CREATE UNIQUE INDEX index_mixins_on_pos ON mixins(pos)')
else
ActiveRecord::Base.connection.execute('ALTER TABLE mixins ADD CONSTRAINT pos_check CHECK (pos > 0)')
end
end

# This table is used to test table names and column names quoting
ActiveRecord::Base.connection.create_table 'table-name' do |t|
t.column :order, :integer
Expand Down Expand Up @@ -784,3 +801,24 @@ def test_nil_position_ordering
assert_equal [2, 3, 1], DefaultScopedMixin.all.map(&:id)
end
end

class DefaultScopedNotNullUniquePositiveConstraintsTest < ActsAsListTestCase
def setup
setup_db null: false, unique: true, positive: true
(1..4).each { |counter| DefaultScopedMixin.create!({pos: counter}) }
end

def test_insert_at
new = DefaultScopedMixin.create
assert_equal 5, new.pos

new.insert_at(1)
assert_equal 1, new.pos

new.insert_at(5)
assert_equal 5, new.pos

new.insert_at(3)
assert_equal 3, new.pos
end
end