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 diff --git a/lib/ancestry/class_methods.rb b/lib/ancestry/class_methods.rb index f95158d9..1f57716a 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 %w(sqlite sqlite3).include?(connection.adapter_name.downcase) + parts.join(' || ') + else + "CONCAT(#{parts.join(', ')})" + end + end + + def sql_cast_as_text column + text_type = if %w(mysql mysql2).include?(connection.adapter_name.downcase) + '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