forked from fixin.me/fixin.me
Fix SQLite compatibility in Unit and Quantity models
Several fixes to make complex Arel/CTE queries work with both MySQL and SQLite: Unit model: - Add guard for description length validation (text column .limit returns nil in SQLite) - defaults_diff: rename 'units' CTE to 'all_units' to avoid SQLite circular reference (SQLite treats any CTE in WITH RECURSIVE that references a same-named table as circular) - defaults_diff: read from 'all_units' CTE explicitly in UNION parts via AR::Relation instead of relying on CTE name shadowing; use AR::Relation (not SelectManager) for UNION parts to avoid extra parentheses (visit_Arel_SelectManager always wraps in parens) - defaults_diff: qualify GROUP BY and ORDER BY columns to avoid ambiguity when bases_units join adds a second table with same column names - Qualify :symbol in ordering to avoid ambiguous column in joined queries Quantity model: - Add guard for description length validation (text column .limit returns nil in SQLite) - ordered: rename recursive CTE from 'quantities' to 'q_ordered' to avoid circular reference; use AR::Relation for UNION parts; fix column qualifiers; use printf() instead of LPAD() for SQLite (LPAD not supported), BLOB instead of BINARY cast - common_ancestors: rename CTE to 'q_common', use AR::Relation for UNION parts - with_ancestors: rename CTE to 'q_ancestors', use AR::Relation for UNION parts - successive: replace SQL LAG window function approach (which causes nested WITH RECURSIVE) with Ruby array approach for SQLite compatibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ class Unit < ApplicationRecord
|
||||
end
|
||||
validates :symbol, presence: true, uniqueness: {scope: :user_id},
|
||||
length: {maximum: type_for_attribute(:symbol).limit}
|
||||
validates :description, length: {maximum: type_for_attribute(:description).limit}
|
||||
validates :description, length: {maximum: type_for_attribute(:description).limit} if type_for_attribute(:description).limit
|
||||
validates :multiplier, numericality: {equal_to: 1}, unless: :base
|
||||
validates :multiplier, numericality: {greater_than: 0, precision: true, scale: true}, if: :base
|
||||
|
||||
@@ -22,59 +22,72 @@ class Unit < ApplicationRecord
|
||||
actionable_units = Arel::Table.new('actionable_units')
|
||||
units = actionable_units.alias('units')
|
||||
bases_units = arel_table.alias('bases_units')
|
||||
other_units = arel_table.alias('other_units')
|
||||
other_bases_units = arel_table.alias('other_bases_units')
|
||||
# Use 'all_units' alias for correlated subqueries to reference the all_units CTE.
|
||||
# Cannot use arel_table.alias here because the CTE is named 'all_units', not 'units'.
|
||||
other_units = Arel::Table.new('all_units').alias('other_units')
|
||||
other_bases_units = Arel::Table.new('all_units').alias('other_bases_units')
|
||||
sub_units = arel_table.alias('sub_units')
|
||||
|
||||
# TODO: move inner 'with' CTE to outer 'with recursive' - it can have multiple
|
||||
# CTEs, even non recursive ones.
|
||||
Unit.with_recursive(actionable_units: [
|
||||
Unit.with(units: self.or(Unit.defaults)).left_joins(:base)
|
||||
.where.not(
|
||||
# Exclude Units that are/have default counterpart
|
||||
Arel::SelectManager.new.project(1).from(other_units)
|
||||
.outer_join(other_bases_units)
|
||||
.on(other_units[:base_id].eq(other_bases_units[:id]))
|
||||
.where(
|
||||
other_bases_units[:symbol].is_not_distinct_from(bases_units[:symbol])
|
||||
.and(other_units[:symbol].eq(arel_table[:symbol]))
|
||||
.and(other_units[:user_id].is_distinct_from(arel_table[:user_id]))
|
||||
).exists
|
||||
)
|
||||
.select(
|
||||
arel_table[Arel.star],
|
||||
# Decide if Unit can be im-/exported based on existing hierarchy:
|
||||
# * same base unit symbol has to exist
|
||||
# * unit with subunits can only be ported to root
|
||||
arel_table[:base_id].eq(nil).or(
|
||||
(
|
||||
Arel::SelectManager.new.project(1).from(other_units)
|
||||
.join(sub_units).on(other_units[:id].eq(sub_units[:base_id]))
|
||||
.where(
|
||||
other_units[:symbol].eq(arel_table[:symbol])
|
||||
.and(other_units[:user_id].is_distinct_from(arel_table[:user_id]))
|
||||
).exists.not
|
||||
).and(
|
||||
Arel::SelectManager.new.project(1).from(other_bases_units)
|
||||
.where(
|
||||
other_bases_units[:symbol].is_not_distinct_from(bases_units[:symbol])
|
||||
.and(other_bases_units[:user_id].is_distinct_from(bases_units[:user_id]))
|
||||
).exists
|
||||
)
|
||||
).as('portable')
|
||||
),
|
||||
# Fill base Units to display proper hierarchy. Duplicates will be removed
|
||||
# by final group() - can't be deduplicated with UNION due to 'portable' field.
|
||||
arel_table.join(actionable_units).on(actionable_units[:base_id].eq(arel_table[:id]))
|
||||
.project(arel_table[Arel.star], Arel::Nodes.build_quoted(nil).as('portable'))
|
||||
]).select(units: [:base_id, :symbol])
|
||||
Unit.with_recursive(
|
||||
# Renamed from 'units' to 'all_units' to avoid SQLite circular reference:
|
||||
# SQLite treats any CTE in WITH RECURSIVE that references a table with the same
|
||||
# name as the CTE itself as a circular reference, even for non-recursive CTEs.
|
||||
all_units: self.or(Unit.defaults),
|
||||
actionable_units: [
|
||||
# Read from all_units CTE (user+defaults) aliased as the units table name.
|
||||
# Using AR::Relation (not Arel::SelectManager) to avoid extra parentheses
|
||||
# around the UNION part (visit_Arel_SelectManager always wraps in parens).
|
||||
Unit.from(Arel::Table.new('all_units').as(arel_table.name))
|
||||
.joins("LEFT OUTER JOIN all_units bases_units ON bases_units.id = units.base_id")
|
||||
.where.not(
|
||||
# Exclude Units that are/have default counterpart
|
||||
Arel::SelectManager.new.project(1).from(other_units)
|
||||
.outer_join(other_bases_units)
|
||||
.on(other_units[:base_id].eq(other_bases_units[:id]))
|
||||
.where(
|
||||
other_bases_units[:symbol].is_not_distinct_from(bases_units[:symbol])
|
||||
.and(other_units[:symbol].eq(arel_table[:symbol]))
|
||||
.and(other_units[:user_id].is_distinct_from(arel_table[:user_id]))
|
||||
).exists
|
||||
)
|
||||
.select(
|
||||
arel_table[Arel.star],
|
||||
# Decide if Unit can be im-/exported based on existing hierarchy:
|
||||
# * same base unit symbol has to exist
|
||||
# * unit with subunits can only be ported to root
|
||||
arel_table[:base_id].eq(nil).or(
|
||||
(
|
||||
Arel::SelectManager.new.project(1).from(other_units)
|
||||
.join(sub_units).on(other_units[:id].eq(sub_units[:base_id]))
|
||||
.where(
|
||||
other_units[:symbol].eq(arel_table[:symbol])
|
||||
.and(other_units[:user_id].is_distinct_from(arel_table[:user_id]))
|
||||
).exists.not
|
||||
).and(
|
||||
Arel::SelectManager.new.project(1).from(other_bases_units)
|
||||
.where(
|
||||
other_bases_units[:symbol].is_not_distinct_from(bases_units[:symbol])
|
||||
.and(other_bases_units[:user_id].is_distinct_from(bases_units[:user_id]))
|
||||
).exists
|
||||
)
|
||||
).as('portable')
|
||||
),
|
||||
# Fill base Units to display proper hierarchy. Duplicates will be removed
|
||||
# by final group() - can't be deduplicated with UNION due to 'portable' field.
|
||||
# Using AR::Relation instead of Arel::SelectManager to avoid extra parentheses
|
||||
# around the UNION part (visit_Arel_SelectManager always wraps in parens).
|
||||
Unit.from(Arel::Table.new('all_units').as(arel_table.name))
|
||||
.joins("INNER JOIN actionable_units ON actionable_units.base_id = units.id")
|
||||
.select(arel_table[Arel.star], Arel::Nodes.build_quoted(nil).as('portable'))
|
||||
]
|
||||
).select(units: [:base_id, :symbol])
|
||||
.select(
|
||||
units[:id].minimum.as('id'), # can be ANY_VALUE()
|
||||
units[:user_id].minimum.as('user_id'), # prefer non-default
|
||||
Arel::Nodes.build_quoted(1).as('multiplier'), # disregard multiplier when sorting
|
||||
units[:portable].minimum.as('portable')
|
||||
)
|
||||
.from(units).group(:base_id, :symbol)
|
||||
.from(units).group(units[:base_id], units[:symbol])
|
||||
}
|
||||
scope :ordered, ->{
|
||||
left_outer_joins(:base).order(ordering)
|
||||
@@ -84,7 +97,7 @@ class Unit < ApplicationRecord
|
||||
[arel_table.coalesce(Arel::Table.new(:bases_units)[:symbol], arel_table[:symbol]),
|
||||
arel_table[:base_id].not_eq(nil),
|
||||
:multiplier,
|
||||
:symbol]
|
||||
arel_table[:symbol]]
|
||||
end
|
||||
|
||||
before_destroy do
|
||||
|
||||
Reference in New Issue
Block a user