From 4e2416ad7dad866f0d6ce9611d07fb80b9ed07d2 Mon Sep 17 00:00:00 2001 From: Anthony Dmitriyev Date: Tue, 11 Aug 2015 15:23:01 +0100 Subject: [PATCH 1/4] Scope for leaves --- lib/ancestry/class_methods.rb | 31 +++++++++++++++++++++++++++ test/concerns/class_methods_test.rb | 33 +++++++++++++++++++++++++++++ test/concerns/scopes_test.rb | 3 +++ 3 files changed, 67 insertions(+) create mode 100644 test/concerns/class_methods_test.rb diff --git a/lib/ancestry/class_methods.rb b/lib/ancestry/class_methods.rb index f95158d9..607c7d8d 100644 --- a/lib/ancestry/class_methods.rb +++ b/lib/ancestry/class_methods.rb @@ -17,6 +17,17 @@ def scope_depth depth_options, depth end end + # Scope that returns all the leaves + def leaves + id_column = "#{table_name}.id" + id_column_as_text = sql_cast_as_text(id_column) + parent_ancestry = sql_concat("#{table_name}.#{ancestry_column}", "'/'", id_column_as_text) + + joins("LEFT JOIN #{table_name} AS c ON c.#{ancestry_column} = #{id_column_as_text} OR c.#{ancestry_column} = #{parent_ancestry}"). + group(id_column). + having('COUNT(c.id) = 0') + end + # Orphan strategy writer def orphan_strategy= orphan_strategy # Check value of orphan strategy, only rootify, adopt, restrict or destroy is allowed @@ -225,5 +236,25 @@ def primary_key_is_an_integer? end end end + + private + + def sql_concat *parts + if ActiveRecord::Base.connection.adapter_name.downcase == 'sqlite' + parts.join(' || ') + else + "CONCAT(#{parts.join(', ')})" + end + end + + def sql_cast_as_text column + text_type = if ActiveRecord::Base.connection.adapter_name.downcase == 'mysql' + 'CHAR' + else + 'TEXT' + end + + "CAST(#{column} AS #{text_type})" + end end end diff --git a/test/concerns/class_methods_test.rb b/test/concerns/class_methods_test.rb new file mode 100644 index 00000000..fd4293bc --- /dev/null +++ b/test/concerns/class_methods_test.rb @@ -0,0 +1,33 @@ +require_relative '../environment' + +class ClassMethodsTest < ActiveSupport::TestCase + def test_sql_concat + AncestryTestDatabase.with_model do |model| + result = model.send(:sql_concat, 'table_name.id', "'/'") + + case ActiveRecord::Base.connection.adapter_name.downcase.to_sym + when :sqlite + assert_equal result, "table_name.id || '/'" + when :mysql + assert_equal result, "CONCAT(table_name.id, '/')" + when :postgresql + assert_equal result, "CONCAT(table_name.id, '/')" + end + end + end + + def text_sql_cast_as_text + AncestryTestDatabase.with_model do |model| + result = model.send(:sql_cast_as_text, 'table_name.id') + + case ActiveRecord::Base.connection.adapter_name.downcase.to_sym + when :sqlite + assert_equal result, 'CAST(table_name.id AS TEXT)' + when :mysql + assert_equal result, 'CAST(table_name.id AS CHAR)' + when :postgresql + assert_equal result, 'CAST(table_name.id AS TEXT)' + end + end + end +end diff --git a/test/concerns/scopes_test.rb b/test/concerns/scopes_test.rb index 83f8f1f2..2965c120 100644 --- a/test/concerns/scopes_test.rb +++ b/test/concerns/scopes_test.rb @@ -6,6 +6,9 @@ def test_scopes # Roots assertion assert_equal roots.map(&:first), model.roots.to_a + # Leaves assertion + assert_equal model.all.select(&:is_childless?), model.leaves.order(:id).to_a + model.all.each do |test_node| # Assertions for ancestors_of named scope assert_equal test_node.ancestors.to_a, model.ancestors_of(test_node).to_a From e84a651503a2c4bc352ce9e9fd853ea3ef46e863 Mon Sep 17 00:00:00 2001 From: Anthony Dmitriyev Date: Sat, 5 Dec 2015 20:36:33 +0000 Subject: [PATCH 2/4] Use UTF8 DB when running tests in TravisCI --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index bd136fbb..699390ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,5 +36,5 @@ services: - postgresql before_script: - - mysql -e 'create database ancestry_test;' || true - - psql -c 'create database ancestry_test;' -U postgres || true + - mysql -e 'CREATE DATABASE ancestry_test CHARACTER SET utf8 COLLATE utf8_general_ci;' || true + - psql -c 'CREATE DATABASE ancestry_test;' -U postgres || true From 7af5b6a28bc1efb987118c3d23bd9b0b15557ec8 Mon Sep 17 00:00:00 2001 From: Anthony Dmitriyev Date: Tue, 10 Jul 2018 21:57:24 +0100 Subject: [PATCH 3/4] fixup! Scope for leaves --- lib/ancestry/class_methods.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ancestry/class_methods.rb b/lib/ancestry/class_methods.rb index 607c7d8d..b4da1d7c 100644 --- a/lib/ancestry/class_methods.rb +++ b/lib/ancestry/class_methods.rb @@ -23,9 +23,9 @@ def leaves id_column_as_text = sql_cast_as_text(id_column) parent_ancestry = sql_concat("#{table_name}.#{ancestry_column}", "'/'", id_column_as_text) - joins("LEFT JOIN #{table_name} AS c ON c.#{ancestry_column} = #{id_column_as_text} OR c.#{ancestry_column} = #{parent_ancestry}"). - group(id_column). - having('COUNT(c.id) = 0') + joins("LEFT JOIN #{table_name} AS c ON c.#{ancestry_column} = #{id_column_as_text} OR c.#{ancestry_column} = #{parent_ancestry}") + .group(id_column) + .having('COUNT(c.id) = 0') end # Orphan strategy writer @@ -249,7 +249,7 @@ def sql_concat *parts def sql_cast_as_text column text_type = if ActiveRecord::Base.connection.adapter_name.downcase == 'mysql' - 'CHAR' + 'CHAR(32)' else 'TEXT' end From 7e532015d06d53d388bbc3d83ca4b03bac367fa1 Mon Sep 17 00:00:00 2001 From: Anthony Dmitriyev Date: Tue, 10 Jul 2018 22:03:06 +0100 Subject: [PATCH 4/4] fixup! Scope for leaves --- lib/ancestry/class_methods.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ancestry/class_methods.rb b/lib/ancestry/class_methods.rb index b4da1d7c..1f57716a 100644 --- a/lib/ancestry/class_methods.rb +++ b/lib/ancestry/class_methods.rb @@ -240,7 +240,7 @@ def primary_key_is_an_integer? private def sql_concat *parts - if ActiveRecord::Base.connection.adapter_name.downcase == 'sqlite' + if %w(sqlite sqlite3).include?(connection.adapter_name.downcase) parts.join(' || ') else "CONCAT(#{parts.join(', ')})" @@ -248,8 +248,8 @@ def sql_concat *parts end def sql_cast_as_text column - text_type = if ActiveRecord::Base.connection.adapter_name.downcase == 'mysql' - 'CHAR(32)' + text_type = if %w(mysql mysql2).include?(connection.adapter_name.downcase) + 'CHAR' else 'TEXT' end