forked from fixin.me/fixin.me
Fix SQLite query compatibility inside WITH clause
Rename model methods to #succeeding.
This commit is contained in:
@@ -19,7 +19,7 @@ class QuantitiesController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
@quantity = current_user.quantities.new(quantity_params)
|
@quantity = current_user.quantities.new(quantity_params)
|
||||||
if @quantity.save
|
if @quantity.save
|
||||||
@before = @quantity.successive
|
@before = @quantity.succeeding
|
||||||
@ancestors = @quantity.ancestors
|
@ancestors = @quantity.ancestors
|
||||||
flash.now.notice = t('.success', quantity: @quantity)
|
flash.now.notice = t('.success', quantity: @quantity)
|
||||||
else
|
else
|
||||||
@@ -45,8 +45,9 @@ class QuantitiesController < ApplicationController
|
|||||||
|
|
||||||
if @quantity.update(permitted)
|
if @quantity.update(permitted)
|
||||||
@ancestors = @quantity.ancestors
|
@ancestors = @quantity.ancestors
|
||||||
@self_and_progenies = @quantity.with_progenies
|
# TODO: simplify into 1 query (see #succeeding comment)
|
||||||
@before = @self_and_progenies.last.successive
|
@quantities = @quantity.with_progenies
|
||||||
|
@before = @quantities.last.succeeding
|
||||||
else
|
else
|
||||||
# Until UI blocks all disallowed reparents, render error messages if present.
|
# Until UI blocks all disallowed reparents, render error messages if present.
|
||||||
flash.now.alert = @quantity.errors.full_messages
|
flash.now.alert = @quantity.errors.full_messages
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class UnitsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
@unit = current_user.units.new(params.expect(Unit::ATTRIBUTES))
|
@unit = current_user.units.new(params.expect(Unit::ATTRIBUTES))
|
||||||
if @unit.save
|
if @unit.save
|
||||||
@before = @unit.successive
|
@before = @unit.succeeding
|
||||||
flash.now.notice = t('.success', unit: @unit)
|
flash.now.notice = t('.success', unit: @unit)
|
||||||
else
|
else
|
||||||
render_errors @unit
|
render_errors @unit
|
||||||
@@ -45,7 +45,7 @@ class UnitsController < ApplicationController
|
|||||||
@previous_base = @unit.base
|
@previous_base = @unit.base
|
||||||
@unit.update!(unit_params)
|
@unit.update!(unit_params)
|
||||||
|
|
||||||
@before = @unit.successive
|
@before = @unit.succeeding
|
||||||
if @unit.multiplier_previously_changed?
|
if @unit.multiplier_previously_changed?
|
||||||
flash.now.notice = t(".multiplier_reset", unit: @unit)
|
flash.now.notice = t(".multiplier_reset", unit: @unit)
|
||||||
end
|
end
|
||||||
|
|||||||
+43
-22
@@ -18,6 +18,9 @@ class Quantity < ApplicationRecord
|
|||||||
end
|
end
|
||||||
validates :name, presence: true, uniqueness: {scope: [:user_id, :parent_id]},
|
validates :name, presence: true, uniqueness: {scope: [:user_id, :parent_id]},
|
||||||
length: {maximum: type_for_attribute(:name).limit}
|
length: {maximum: type_for_attribute(:name).limit}
|
||||||
|
# FIXME: length validation for :text type column is probably wrong; reported
|
||||||
|
# limit is in bytes (at least for MySQL) and check is for #length (i.e.
|
||||||
|
# characters); this doesn't seem to apply to :string (implemented as varchar)
|
||||||
validates :description, length: {maximum: type_for_attribute(:description).limit}
|
validates :description, length: {maximum: type_for_attribute(:description).limit}
|
||||||
|
|
||||||
# Update :depths of progenies after parent change
|
# Update :depths of progenies after parent change
|
||||||
@@ -62,26 +65,42 @@ class Quantity < ApplicationRecord
|
|||||||
scope :defaults, ->{ where(user: nil) }
|
scope :defaults, ->{ where(user: nil) }
|
||||||
|
|
||||||
# Return: ordered [sub]hierarchy
|
# Return: ordered [sub]hierarchy
|
||||||
scope :ordered, ->(root: nil, include_root: true) {
|
scope :ordered, ->(**kwargs) {
|
||||||
numbered = Arel::Table.new('numbered')
|
with_path(**kwargs).order(arel_table[:path])
|
||||||
|
}
|
||||||
|
|
||||||
self.model.with(numbered: numbered(:parent_id, :name)).with_recursive(arel_table.name => [
|
# TODO: sort recursive CTE using ORDER BY; it's supported by SQLite, so
|
||||||
numbered.project(
|
# drop MySQL until it is available (?)
|
||||||
numbered[Arel.star],
|
# Only ORDER BY allows proper i.e. not placing limits on user data and
|
||||||
numbered.cast(numbered[:child_number], 'BINARY').as('path')
|
# efficient with regard to paging (LIMIT/OFFSET) depth-first search.
|
||||||
).where(numbered[root && include_root ? :id : :parent_id].eq(root)),
|
# Sorting based on numbering is less elegant/efficient (MySQL: ROW_NUMBER cannot
|
||||||
numbered.project(
|
# be used inside recursive CTE and needs separate SELECT,
|
||||||
numbered[Arel.star],
|
# https://bugs.mysql.com/bug.php?id=96538).
|
||||||
arel_table[:path].concat(numbered[:child_number])
|
# Sorting based on pathname requires I) padding to maximum column length or
|
||||||
).join(arel_table).on(numbered[:parent_id].eq(arel_table[:id]))
|
# II) delimiter which a) cannot be used in sorted column and b) sorts before
|
||||||
]).order(arel_table[:path])
|
# anything else (this adds complexity; NULL cannot be used when sorting).
|
||||||
|
# Sorting based on ARRAY is only available on PostgreSQL.
|
||||||
|
scope :with_path, ->(root: nil, include_root: true) {
|
||||||
|
with_number = Arel::Table.new('with_number')
|
||||||
|
|
||||||
|
self.model.with(with_number: with_number(:parent_id, :name))
|
||||||
|
.with_recursive(arel_table.name => [
|
||||||
|
with_number.project(
|
||||||
|
with_number[Arel.star],
|
||||||
|
with_number.cast(with_number[:number], 'BINARY').as('path')
|
||||||
|
).where(with_number[root && include_root ? :id : :parent_id].eq(root)),
|
||||||
|
with_number.project(
|
||||||
|
with_number[Arel.star],
|
||||||
|
arel_table[:path].concat(with_number[:number])
|
||||||
|
).join(arel_table).on(with_number[:parent_id].eq(arel_table[:id]))
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO: extract named functions to custom Arel extension
|
# TODO: extract named functions to custom Arel extension
|
||||||
# NOTE: once recursive queries allow use of window functions, this scope can
|
# NOTE: once recursive queries allow use of window functions, this scope can
|
||||||
# be merged with :ordered
|
# be merged with :ordered
|
||||||
# https://gist.github.com/ProGM/c6df08da14708dcc28b5ca325df37ceb#extending-arel
|
# https://gist.github.com/ProGM/c6df08da14708dcc28b5ca325df37ceb#extending-arel
|
||||||
scope :numbered, ->(parent_column, order_column) {
|
scope :with_number, ->(parent_column, order_column) {
|
||||||
select(
|
select(
|
||||||
arel_table[Arel.star],
|
arel_table[Arel.star],
|
||||||
Arel::Nodes::NamedFunction.new(
|
Arel::Nodes::NamedFunction.new(
|
||||||
@@ -94,7 +113,7 @@ class Quantity < ApplicationRecord
|
|||||||
).from(arel_table),
|
).from(arel_table),
|
||||||
Arel::Nodes.build_quoted('0')
|
Arel::Nodes.build_quoted('0')
|
||||||
],
|
],
|
||||||
).as('child_number')
|
).as('number')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,15 +151,17 @@ class Quantity < ApplicationRecord
|
|||||||
.order(arel_table[:depth].desc)
|
.order(arel_table[:depth].desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Return: successive record in order of appearance; used for partial view reload
|
# Return: succeeding record in order of appearance, used for partial view reload.
|
||||||
def successive
|
# TODO: add scope returning succeeding element for multiple records
|
||||||
|
def succeeding
|
||||||
quantities = Quantity.arel_table
|
quantities = Quantity.arel_table
|
||||||
Quantity.with(
|
Quantity.with(lagging_quantities: user.quantities.with_path.select(
|
||||||
quantities: user.quantities.ordered.select(
|
quantities[Arel.star],
|
||||||
quantities[Arel.star],
|
# For SQLite 'OVER () ORDER BY x' is not equivalent to 'OVER (ORDER BY x)'.
|
||||||
Arel::Nodes::NamedFunction.new('LAG', [quantities[:id]]).over.as('lag_id')
|
Arel::Nodes::NamedFunction.new('LAG', [quantities[:id]])
|
||||||
)
|
.over(Arel::Nodes::Window.new.order(Quantity.ordered.order_values)).as('lag_id')
|
||||||
).where(quantities[:lag_id].eq(id)).first
|
)).from(Arel::Table.new(:lagging_quantities).as(:quantities))
|
||||||
|
.where(quantities[:lag_id].eq(id)).take
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_progenies
|
def with_progenies
|
||||||
|
|||||||
+4
-3
@@ -3,6 +3,7 @@ class Unit < ApplicationRecord
|
|||||||
|
|
||||||
belongs_to :user, optional: true
|
belongs_to :user, optional: true
|
||||||
belongs_to :base, optional: true, class_name: "Unit"
|
belongs_to :base, optional: true, class_name: "Unit"
|
||||||
|
# TODO: use #alias_attribute after Rails upgrade?
|
||||||
alias_method :parent, :base
|
alias_method :parent, :base
|
||||||
has_many :subunits, class_name: "Unit", inverse_of: :base,
|
has_many :subunits, class_name: "Unit", inverse_of: :base,
|
||||||
dependent: :restrict_with_error
|
dependent: :restrict_with_error
|
||||||
@@ -111,13 +112,13 @@ class Unit < ApplicationRecord
|
|||||||
.update!(base: recipient_base, **params)
|
.update!(base: recipient_base, **params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def successive
|
def succeeding
|
||||||
units = Unit.arel_table
|
units = Unit.arel_table
|
||||||
Unit.with(units_with_lag: user.units.left_outer_joins(:base).select(
|
Unit.with(lagging_units: user.units.left_outer_joins(:base).select(
|
||||||
units[Arel.star],
|
units[Arel.star],
|
||||||
Arel::Nodes::NamedFunction.new('LAG', [units[:id]])
|
Arel::Nodes::NamedFunction.new('LAG', [units[:id]])
|
||||||
.over(Arel::Nodes::Window.new.order(Unit.ordered.order_values)).as('lag_id')
|
.over(Arel::Nodes::Window.new.order(Unit.ordered.order_values)).as('lag_id')
|
||||||
)).from(Arel::Table.new(:units_with_lag).as(:units))
|
)).from(Arel::Table.new(:lagging_units).as(:units))
|
||||||
.where(units[:lag_id].eq(id)).take
|
.where(units[:lag_id].eq(id)).take
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<% @previous_ancestors.union(@ancestors).each do |ancestor| %>
|
<% @previous_ancestors.union(@ancestors).each do |ancestor| %>
|
||||||
<%= turbo_stream.replace ancestor %>
|
<%= turbo_stream.replace ancestor %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% @self_and_progenies.each do |quantity| %>
|
<% @quantities.each do |quantity| %>
|
||||||
<%= turbo_stream.remove quantity %>
|
<%= turbo_stream.remove quantity %>
|
||||||
<%= turbo_stream.before @before || :endmark, quantity %>
|
<%= turbo_stream.before @before || :endmark, quantity %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
Reference in New Issue
Block a user