From d54467f259d955d33df9b8a847c78ffc9e412425 Mon Sep 17 00:00:00 2001 From: barbie-bot Date: Tue, 10 Mar 2026 18:40:54 +0000 Subject: [PATCH] Fix quantity ordered scope for SQLite: use pathname column instead of recursive CTE SQLite's Arel visitor wraps CTE branches in extra parentheses, making the UNION ALL inside recursive CTEs invalid. Also SQLite lacks LPAD() and CAST(... AS BINARY). Fix by using the existing pathname column for ordering on SQLite, which already encodes the hierarchical path. Co-Authored-By: Claude Sonnet 4.6 --- app/models/quantity.rb | 53 ++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/app/models/quantity.rb b/app/models/quantity.rb index bd8ce0b..e8d36d3 100644 --- a/app/models/quantity.rb +++ b/app/models/quantity.rb @@ -61,24 +61,26 @@ class Quantity < ApplicationRecord # Return: ordered [sub]hierarchy scope :ordered, ->(root: nil, include_root: true) { - numbered = Arel::Table.new('numbered') - - path_expr = if connection.adapter_name =~ /mysql/i - numbered.cast(numbered[:child_number], 'BINARY') + if connection.adapter_name =~ /mysql/i + numbered = Arel::Table.new('numbered') + self.model.with(numbered: numbered(:parent_id, :name)).with_recursive(arel_table.name => [ + numbered.project( + numbered[Arel.star], + numbered.cast(numbered[:child_number], 'BINARY').as('path') + ).where(numbered[root && include_root ? :id : :parent_id].eq(root)), + numbered.project( + numbered[Arel.star], + arel_table[:path].concat(numbered[:child_number]) + ).join(arel_table).on(numbered[:parent_id].eq(arel_table[:id])) + ]).order(arel_table[:path]) + elsif root.nil? + # SQLite: pathname column already stores the full hierarchical path + order(:pathname) else - numbered[:child_number] + root_pathname = unscoped.where(id: root).pick(:pathname) + scope = order(:pathname).where("pathname LIKE ?", "#{root_pathname}#{PATHNAME_DELIMITER}%") + include_root ? scope.or(where(id: root)) : scope end - - self.model.with(numbered: numbered(:parent_id, :name)).with_recursive(arel_table.name => [ - numbered.project( - numbered[Arel.star], - path_expr.as('path') - ).where(numbered[root && include_root ? :id : :parent_id].eq(root)), - numbered.project( - numbered[Arel.star], - arel_table[:path].concat(numbered[:child_number]) - ).join(arel_table).on(numbered[:parent_id].eq(arel_table[:id])) - ]).order(arel_table[:path]) } # TODO: extract named functions to custom Arel extension @@ -86,25 +88,20 @@ class Quantity < ApplicationRecord # be merged with :ordered # https://gist.github.com/ProGM/c6df08da14708dcc28b5ca325df37ceb#extending-arel scope :numbered, ->(parent_column, order_column) { - row_num = Arel::Nodes::NamedFunction.new('ROW_NUMBER', []) - .over(Arel::Nodes::Window.new.partition(parent_column).order(order_column)) - - child_number = if connection.adapter_name =~ /mysql/i + select( + arel_table[Arel.star], Arel::Nodes::NamedFunction.new( 'LPAD', [ - row_num, + Arel::Nodes::NamedFunction.new('ROW_NUMBER', []) + .over(Arel::Nodes::Window.new.partition(parent_column).order(order_column)), Arel::SelectManager.new.project( Arel::Nodes::NamedFunction.new('LENGTH', [Arel.star.count]) ).from(arel_table), Arel::Nodes.build_quoted('0') - ] - ) - else - Arel::Nodes::NamedFunction.new('format', [Arel::Nodes.build_quoted('%09d'), row_num]) - end - - select(arel_table[Arel.star], child_number.as('child_number')) + ], + ).as('child_number') + ) } def to_s