From c908063212caabcba631e70c83386da26a018380 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Thu, 16 Jan 2025 17:14:52 +0100 Subject: [PATCH] Persist Quantity :depth instead of computing it on the fly --- app/models/quantity.rb | 58 ++++++++++++++----- .../20250104194343_create_quantities.rb | 1 + db/schema.rb | 1 + 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/app/models/quantity.rb b/app/models/quantity.rb index 7490221..b733004 100644 --- a/app/models/quantity.rb +++ b/app/models/quantity.rb @@ -17,8 +17,46 @@ class Quantity < ApplicationRecord length: {maximum: type_for_attribute(:name).limit} validates :description, length: {maximum: type_for_attribute(:description).limit} - # Not persisted attribute - attribute :depth, :integer + # Update `depth`s of progenies after parent change + after_update if: :depth_previously_changed? do + quantities = Quantity.arel_table + selected = Arel::Table.new('selected') + + self.class.connection.update( + Arel::UpdateManager.new.table( + Arel::Nodes::JoinSource.new( + quantities, + [ + quantities.create_join( + # TODO: user .with(quanities: user.quantities) once the '?' problem is fixed + Quantity.with_recursive(selected: [ + quantities.project(quantities[:id], quantities[:depth]) + .where(quantities[:id].eq(id).and(quantities[:user_id].eq(user.id))), + quantities.project(quantities[:id], selected[:depth] + 1) + .join(selected).on(selected[:id].eq(quantities[:parent_id])) + ]).select(selected[Arel.star]).from(selected).arel.as('selected'), + quantities.create_on(quantities[:id].eq(selected[:id])) + ) + ] + ) + ).set(quantities[:depth] => selected[:depth]), + "#{self.class} Update All" + ) + end + + def parent=(value) + super + self[:depth] = parent&.depth&.succ || 0 + end + + def parent_id=(value) + super + self[:depth] = parent&.depth&.succ || 0 + end + + def depth=(value) + raise ActiveRecord::ReadonlyAttributeError + end scope :defaults, ->{ where(user: nil) } @@ -30,12 +68,10 @@ class Quantity < ApplicationRecord numbered.project( numbered[Arel.star], numbered.cast(numbered[:child_number], 'BINARY').as('path'), - Arel::Nodes.build_quoted(root&.depth || 0).as('depth') ).where(root.nil? ? numbered[:parent_id].eq(nil) : numbered[:id].eq(root.id)), numbered.project( numbered[Arel.star], arel_table[:path].concat(numbered[:child_number]), - arel_table[:depth] + 1 ).join(arel_table).on(numbered[:parent_id].eq(arel_table[:id])) ]).order(arel_table[:path]) } @@ -93,18 +129,10 @@ class Quantity < ApplicationRecord selected = Arel::Table.new('selected') model.with(selected: self).with_recursive(arel_table.name => [ - selected.project(selected[Arel.star], Arel::Nodes.build_quoted(0).as('depth')) - .where(selected[:id].eq(of)), - # Ancestors are listed bottom up, so it's impossible to know depth at the - # start. Start with depth = 0 and count downwards, then adjust by the - # amount needed to set biggest negative depth to 0. - selected.project(selected[Arel.star], arel_table[:depth] - 1) + selected.project(selected[Arel.star]).where(selected[:id].eq(of)), + selected.project(selected[Arel.star]) .join(arel_table).on(selected[:id].eq(arel_table[:parent_id])) - ]).select( - arel_table[Arel.star], - (arel_table[:depth] + Arel::SelectManager.new.project(Arel.star.count).from(arel_table) - 1) - .as('depth') - ).order(arel_table[:depth]) + ]) } # Return: ancestors of (possibly destroyed) self diff --git a/db/migrate/20250104194343_create_quantities.rb b/db/migrate/20250104194343_create_quantities.rb index 1bb31d5..7fc63cf 100644 --- a/db/migrate/20250104194343_create_quantities.rb +++ b/db/migrate/20250104194343_create_quantities.rb @@ -5,6 +5,7 @@ class CreateQuantities < ActiveRecord::Migration[7.2] t.string :name, null: false, limit: 31 t.text :description t.references :parent, foreign_key: {to_table: :quantities} + t.integer :depth, null: false, default: 0 t.timestamps null: false end diff --git a/db/schema.rb b/db/schema.rb index 583deb0..552f1b2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -16,6 +16,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_01_04_194343) do t.string "name", limit: 31, null: false t.text "description" t.bigint "parent_id" + t.integer "depth", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["parent_id"], name: "index_quantities_on_parent_id"