From 83168092f151c53c25932bc071bebf33e0fbcd42 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Fri, 8 May 2026 22:09:37 +0200 Subject: [PATCH] Fix SQLite query compatibility inside WITH clause SQLite3::SQLException: circular reference --- app/models/unit.rb | 26 ++++++++--------- test/system/units_test.rb | 24 +++++++++++++++ test/test_helper.rb | 61 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 14 deletions(-) diff --git a/app/models/unit.rb b/app/models/unit.rb index 18ef123..5f5865c 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -77,16 +77,14 @@ class Unit < ApplicationRecord .from(units).group(:base_id, :symbol) } scope :ordered, ->{ - left_outer_joins(:base).order(ordering) + left_outer_joins(:base).order([ + arel_table.coalesce(Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]), + arel_table[:base_id].not_eq(nil), + arel_table[:multiplier], + arel_table[:symbol] + ]) } - def self.ordering - [arel_table.coalesce(Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]), - arel_table[:base_id].not_eq(nil), - :multiplier, - :symbol] - end - before_destroy do # TODO: disallow destruction if any object depends on this unit nil @@ -114,11 +112,11 @@ class Unit < ApplicationRecord def successive units = Unit.arel_table - lead = Arel::Nodes::NamedFunction.new('LAG', [units[:id]]) - window = Arel::Nodes::Window.new.order(*Unit.ordering) - lag_id = lead.over(window).as('lag_id') - Unit.with( - units: user.units.left_outer_joins(:base).select(units[Arel.star], lag_id) - ).where(units[:lag_id].eq(id)).first + Unit.with(units_with_lag: user.units.left_outer_joins(:base).select( + units[Arel.star], + Arel::Nodes::NamedFunction.new('LAG', [units[:id]]) + .over(Arel::Nodes::Window.new.order(Unit.ordered.order_values)).as('lag_id') + )).from(Arel::Table.new(:units_with_lag).as(:units)) + .where(units[:lag_id].eq(id)).take end end diff --git a/test/system/units_test.rb b/test/system/units_test.rb index 1b1ce23..5bf4dda 100644 --- a/test/system/units_test.rb +++ b/test/system/units_test.rb @@ -74,6 +74,30 @@ class UnitsTest < ApplicationSystemTestCase assert_equal values, Unit.last.attributes.slice(*values.keys) end + test "create updates view in order" do + # Destroy and re-create unit to verify its index position is unchanged. + sign_in(user: users.select { |u| u.confirmed? && u.units.many? }.sample) + + link = all(:link_or_button, exact_text: t('units.unit.destroy')).sample + symbol = link.ancestor('tr').first(:link).text + index = link.ancestor('tbody').all('tr').index { |e| e.first(:link).text == symbol } + unit = @user.units.find_by(symbol: symbol) + + link.click + if unit.base_id? + find_link(unit.base.symbol).ancestor('tr').click_on(t('units.unit.new_subunit')) + fill_in 'unit[multiplier]', with: unit.multiplier + else + click_on t('units.index.new_unit') + end + fill_in 'unit[symbol]', with: unit.symbol + click_on t('helpers.submit.create') + + within "tbody > tr:nth-child(#{index+1})" do + assert_selector :link, exact_text: symbol + end + end + test "new and edit on validation error" do # It's not possible to cause validation error on :edit with single unit LINK_LABELS.delete(:edit) unless @user.units.count > 1 diff --git a/test/test_helper.rb b/test/test_helper.rb index 4b5d79d..2894b00 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -35,6 +35,67 @@ class ActiveSupport::TestCase result end + # Range based string generation. Not finished, but saved as a potential + # reference for future work. To work properly it needs to sort in collation + # order of database. + #def random_between(from, to, maxlength) + # # TODO: sort UNICODE_CHARS + # byebug + # result = '' + # maxlength.times do |i| + # case + # when from[i] == to[i] + # result += from[i] + # else + # from_index = UNICODE_CHARS.bsearch_index(from[i] || UNICODE_CHARS[0]) + # to_index = UNICODE_CHARS.bsearch_index(to[i]) + # index = rand(from_index..to_index) + # case + # when index == from_index + # result += UNICODE_CHARS[index] + # from[i+1..].each_char do |c| + # from_index = UNICODE_CHARS.bsearch_index(from[i]) + # index = rand(from_index..UNICODE_CHARS.length-1) + # result += UNICODE_CHARS[index] + # break if index != from_index + # end + # if result == from + # if result.length < maxlength + # result += UNICODE_CHARS.sample + # else + # # TODO: succ result[i..] + # # raise if result == to + # end + # end + # break + # when index == to_index + # result += UNICODE_CHARS[index] + # to[i+1..].each_char do |c| + # to_index = UNICODE_CHARS.bsearch_index(to[i]) + # index = rand(-1..to_index) + # break if index == -1 + # result += UNICODE_CHARS[index] + # break if index != to_index + # end + # if result == to + # if result.length > i+1 + # result = result[..-2] + # else + # # TODO: prev result[i..] + # # raise if result == from + # end + # end + # break + # else + # result += UNICODE_CHARS[index] + # break + # end + # end + # end + # return result += UNICODE_CHARS.sample(rand(0..maxlength-i-1)).join + # # if result == from/to ... + #end + def deep_rand(*args) case args when Array