Back to Repositories

Testing Search Implementation and Query Building in Ransack

This test suite validates the core search functionality of the Ransack gem, focusing on search initialization, condition building, and result generation. The tests cover search parameter handling, condition creation, and complex query building capabilities.

Test Coverage Overview

The test suite provides comprehensive coverage of Ransack’s search functionality, including:

  • Search initialization and parameter handling
  • Empty/nil condition handling
  • Whitespace stripping configuration
  • Complex condition building with associations
  • Polymorphic relationship handling
  • Sort functionality and direction processing

Implementation Analysis

The testing approach uses RSpec to validate search behavior through multiple contexts:

The tests employ mock expectations and context-specific setups to verify proper object initialization, condition building, and query generation. Key patterns include setting up test data, validating SQL output, and checking search result integrity across different scenarios.

Technical Details

Testing tools and configuration:

  • RSpec as the testing framework
  • ActiveRecord test models (Person, Article, Note)
  • Mock objects for parameter validation
  • SQL query validation
  • Custom predicate configuration
  • Database-specific testing for PostgreSQL features

Best Practices Demonstrated

The test suite demonstrates several testing best practices:

  • Comprehensive edge case coverage
  • Isolation of database-specific functionality
  • Clear test organization and naming
  • Proper setup and teardown of test state
  • Validation of both positive and negative cases
  • Testing of configuration options and their effects

activerecord-hackery/ransack

spec/ransack/search_spec.rb

            
require 'spec_helper'

module Ransack
  describe Search do
    describe '#initialize' do
      it 'removes empty conditions before building' do
        expect_any_instance_of(Search).to receive(:build).with({})
        Search.new(Person, name_eq: '')
      end

      it 'keeps conditions with a false value before building' do
        expect_any_instance_of(Search).to receive(:build)
        .with({ 'name_eq' => false })
        Search.new(Person, name_eq: false)
      end

      it 'keeps conditions with a value before building' do
        expect_any_instance_of(Search).to receive(:build)
        .with({ 'name_eq' => 'foobar' })
        Search.new(Person, name_eq: 'foobar')
      end

      context 'whitespace stripping' do
        context 'when whitespace_strip option is true' do
          before do
            Ransack.configure { |c| c.strip_whitespace = true }
          end

          it 'strips leading & trailing whitespace before building' do
            expect_any_instance_of(Search).to receive(:build)
            .with({ 'name_eq' => 'foobar' })
            Search.new(Person, name_eq: '   foobar     ')
          end
        end

        context 'when whitespace_strip option is false' do
          before do
            Ransack.configure { |c| c.strip_whitespace = false }
          end

          it 'doesn\'t strip leading & trailing whitespace before building' do
            expect_any_instance_of(Search).to receive(:build)
            .with({ 'name_eq' => '   foobar     ' })
            Search.new(Person, name_eq: '   foobar     ')
          end
        end

        it 'strips leading & trailing whitespace when strip_whitespace search parameter is true' do
          expect_any_instance_of(Search).to receive(:build)
          .with({ 'name_eq' => 'foobar' })
          Search.new(Person, { name_eq: '   foobar     ' }, { strip_whitespace: true })
        end

        it 'doesn\'t strip leading & trailing whitespace when strip_whitespace search parameter is false' do
          expect_any_instance_of(Search).to receive(:build)
          .with({ 'name_eq' => '   foobar     ' })
          Search.new(Person, { name_eq: '   foobar     ' }, { strip_whitespace: false })
        end
      end

      it 'removes empty suffixed conditions before building' do
        expect_any_instance_of(Search).to receive(:build).with({})
        Search.new(Person, name_eq_any: [''])
      end

      it 'keeps suffixed conditions with a false value before building' do
        expect_any_instance_of(Search).to receive(:build)
        .with({ 'name_eq_any' => [false] })
        Search.new(Person, name_eq_any: [false])
      end

      it 'keeps suffixed conditions with a value before building' do
        expect_any_instance_of(Search).to receive(:build)
        .with({ 'name_eq_any' => ['foobar'] })
        Search.new(Person, name_eq_any: ['foobar'])
      end

      it 'does not raise exception for string :params argument' do
        expect { Search.new(Person, '') }.not_to raise_error
      end

      it 'accepts a context option' do
        shared_context = Context.for(Person)
        s1 = Search.new(Person, { name_eq: 'A' }, context: shared_context)
        s2 = Search.new(Person, { name_eq: 'B' }, context: shared_context)
        expect(s1.context).to be s2.context
      end
    end

    describe '#build' do
      it 'creates conditions for top-level attributes' do
        s = Search.new(Person, name_eq: 'Ernie')
        condition = s.base[:name_eq]
        expect(condition).to be_a Nodes::Condition
        expect(condition.predicate.name).to eq 'eq'
        expect(condition.attributes.first.name).to eq 'name'
        expect(condition.value).to eq 'Ernie'
      end

      it 'creates conditions for association attributes' do
        s = Search.new(Person, children_name_eq: 'Ernie')
        condition = s.base[:children_name_eq]
        expect(condition).to be_a Nodes::Condition
        expect(condition.predicate.name).to eq 'eq'
        expect(condition.attributes.first.name).to eq 'children_name'
        expect(condition.value).to eq 'Ernie'
      end

      it 'creates conditions for polymorphic belongs_to association attributes' do
        s = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie')
        condition = s.base[:notable_of_Person_type_name_eq]
        expect(condition).to be_a Nodes::Condition
        expect(condition.predicate.name).to eq 'eq'
        expect(condition.attributes.first.name)
          .to eq 'notable_of_Person_type_name'
        expect(condition.value).to eq 'Ernie'
      end

      it 'creates conditions for multiple polymorphic belongs_to association
        attributes' do
        s = Search.new(Note,
          notable_of_Person_type_name_or_notable_of_Article_type_title_eq: 'Ernie')
        condition = s.
          base[:notable_of_Person_type_name_or_notable_of_Article_type_title_eq]
        expect(condition).to be_a Nodes::Condition
        expect(condition.predicate.name).to eq 'eq'
        expect(condition.attributes.first.name)
          .to eq 'notable_of_Person_type_name'
        expect(condition.attributes.last.name)
          .to eq 'notable_of_Article_type_title'
        expect(condition.value).to eq 'Ernie'
      end

      it 'creates conditions for aliased attributes',
      if: Ransack::SUPPORTS_ATTRIBUTE_ALIAS do
        s = Search.new(Person, full_name_eq: 'Ernie')
        condition = s.base[:full_name_eq]
        expect(condition).to be_a Nodes::Condition
        expect(condition.predicate.name).to eq 'eq'
        expect(condition.attributes.first.name).to eq 'full_name'
        expect(condition.value).to eq 'Ernie'
      end

      it 'preserves default scope and conditions for associations' do
        s = Search.new(Person, published_articles_title_eq: 'Test')
        expect(s.result.to_sql).to include 'default_scope'
        expect(s.result.to_sql).to include 'published'
      end

      # The failure/oversight in Ransack::Nodes::Condition#arel_predicate or deeper is beyond my understanding of the structures
      it 'preserves (inverts) default scope and conditions for negative subqueries' do
        # the positive case (published_articles_title_eq) is
        # SELECT "people".* FROM "people"
        # LEFT OUTER JOIN "articles" ON "articles"."person_id" = "people"."id"
        #   AND "articles"."published" = 't'
        #   AND ('default_scope' = 'default_scope')
        # WHERE "articles"."title" = 'Test' ORDER BY "people"."id" DESC
        #
        # negative case was
        # SELECT "people".* FROM "people" WHERE "people"."id" NOT IN (
        #   SELECT "articles"."person_id" FROM "articles"
        #   WHERE "articles"."person_id" = "people"."id"
        #     AND NOT ("articles"."title" != 'Test')
        # ) ORDER BY "people"."id" DESC
        #
        # Should have been like
        # SELECT "people".* FROM "people" WHERE "people"."id" NOT IN (
        #   SELECT "articles"."person_id" FROM "articles"
        #   WHERE "articles"."person_id" = "people"."id"
        #     AND "articles"."title" = 'Test' AND "articles"."published" = 't' AND ('default_scope' = 'default_scope')
        # ) ORDER BY "people"."id" DESC
        #
        # With tenanting (eg default_scope with column reference), NOT IN should be like
        # SELECT "people".* FROM "people" WHERE "people"."tenant_id" = 'tenant_id' AND "people"."id" NOT IN (
        #   SELECT "articles"."person_id" FROM "articles"
        #   WHERE "articles"."person_id" = "people"."id"
        #     AND "articles"."tenant_id" = 'tenant_id'
        #     AND "articles"."title" = 'Test' AND "articles"."published" = 't' AND ('default_scope' = 'default_scope')
        # ) ORDER BY "people"."id" DESC

        pending("spec should pass, but I do not know how/where to fix lib code")
        s = Search.new(Person, published_articles_title_not_eq: 'Test')
        expect(s.result.to_sql).to include 'default_scope'
        expect(s.result.to_sql).to include 'published'
      end

      it 'discards empty conditions' do
        s = Search.new(Person, children_name_eq: '')
        condition = s.base[:children_name_eq]
        expect(condition).to be_nil
      end

      it 'accepts base grouping condition as an option' do
        expect(Nodes::Grouping).to receive(:new).with(kind_of(Context), 'or')
        Search.new(Person, {}, { grouping: 'or' })
      end

      it 'accepts arrays of groupings' do
        s = Search.new(Person,
          g: [
            { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
            { m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' },
          ]
        )
        ors = s.groupings
        expect(ors.size).to eq(2)
        or1, or2 = ors
        expect(or1).to be_a Nodes::Grouping
        expect(or1.combinator).to eq 'or'
        expect(or2).to be_a Nodes::Grouping
        expect(or2.combinator).to eq 'or'
      end

      it 'accepts attributes hashes for groupings' do
        s = Search.new(Person,
          g: {
            '0' => { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
            '1' => { m: 'or', name_eq: 'Bert',  children_name_eq: 'Bert' },
          }
        )
        ors = s.groupings
        expect(ors.size).to eq(2)
        or1, or2 = ors
        expect(or1).to be_a Nodes::Grouping
        expect(or1.combinator).to eq 'or'
        expect(or2).to be_a Nodes::Grouping
        expect(or2.combinator).to eq 'or'
      end

      it 'accepts attributes hashes for conditions' do
        s = Search.new(Person,
          c: {
            '0' => { a: ['name'], p: 'eq', v: ['Ernie'] },
            '1' => {
                     a: ['children_name', 'parent_name'],
                     p: 'eq', v: ['Ernie'], m: 'or'
                   }
          }
        )
        conditions = s.base.conditions
        expect(conditions.size).to eq(2)
        expect(conditions.map { |c| c.class })
        .to eq [Nodes::Condition, Nodes::Condition]
      end

      it 'creates conditions for custom predicates that take arrays' do
        Ransack.configure do |config|
          config.add_predicate 'ary_pred', wants_array: true
        end

        s = Search.new(Person, name_ary_pred: ['Ernie', 'Bert'])
        condition = s.base[:name_ary_pred]
        expect(condition).to be_a Nodes::Condition
        expect(condition.predicate.name).to eq 'ary_pred'
        expect(condition.attributes.first.name).to eq 'name'
        expect(condition.value).to eq ['Ernie', 'Bert']
      end

      it 'does not evaluate the query on #inspect' do
        s = Search.new(Person, children_id_in: [1, 2, 3])
        expect(s.inspect).not_to match /ActiveRecord/
      end

      context 'with an invalid condition' do
        subject { Search.new(Person, unknown_attr_eq: 'Ernie') }

        context 'when ignore_unknown_conditions configuration option is false' do
          before do
            Ransack.configure { |c| c.ignore_unknown_conditions = false }
          end

          specify { expect { subject }.to raise_error ArgumentError }
        end

        context 'when ignore_unknown_conditions configuration option is true' do
          before do
            Ransack.configure { |c| c.ignore_unknown_conditions = true }
          end

          specify { expect { subject }.not_to raise_error }
        end

        subject(:with_ignore_unknown_conditions_false) {
          Search.new(Person,
            { unknown_attr_eq: 'Ernie' },
            { ignore_unknown_conditions: false }
          )
        }

        subject(:with_ignore_unknown_conditions_true) {
          Search.new(Person,
            { unknown_attr_eq: 'Ernie' },
            { ignore_unknown_conditions: true }
          )
        }

        context 'when ignore_unknown_conditions search parameter is absent' do
          specify { expect { subject }.not_to raise_error }
        end

        context 'when ignore_unknown_conditions search parameter is false' do
          specify { expect { with_ignore_unknown_conditions_false }.to raise_error ArgumentError }
        end

        context 'when ignore_unknown_conditions search parameter is true' do
          specify { expect { with_ignore_unknown_conditions_true }.not_to raise_error }
        end
      end

      it 'does not modify the parameters' do
        params = { name_eq: '' }
        expect { Search.new(Person, params) }.not_to change { params }
      end

      context "ransackable_scope" do
        around(:each) do |example|
          Person.define_singleton_method(:name_eq) do |name|
            self.where(name: name)
          end

          begin
            example.run
          ensure
            Person.singleton_class.undef_method :name_eq
          end
        end

        it "is prioritized over base predicates" do
          allow(Person).to receive(:ransackable_scopes)
            .and_return(Person.ransackable_scopes + [:name_eq])

          s = Search.new(Person, name_eq: "Johny")
          expect(s.instance_variable_get(:@scope_args)["name_eq"]).to eq("Johny")
          expect(s.base[:name_eq]).to be_nil
        end
      end

    end

    describe '#result' do
      let(:people_name_field) {
        "#{quote_table_name("people")}.#{quote_column_name("name")}"
      }
      let(:children_people_name_field) {
        "#{quote_table_name("children_people")}.#{quote_column_name("name")}"
      }
      let(:notable_type_field) {
        "#{quote_table_name("notes")}.#{quote_column_name("notable_type")}"
      }
      it 'evaluates conditions contextually' do
        s = Search.new(Person, children_name_eq: 'Ernie')
        expect(s.result).to be_an ActiveRecord::Relation
        expect(s.result.to_sql).to match /#{
          children_people_name_field} = 'Ernie'/
      end

      it 'use appropriate table alias' do
        s = Search.new(Person, {
          name_eq: "person_name_query",
          articles_title_eq: "person_article_title_query",
          parent_name_eq: "parent_name_query",
          parent_articles_title_eq: 'parents_article_title_query'
        }).result

        real_query = remove_quotes_and_backticks(s.to_sql)

        expect(real_query)
                .to match(%r{LEFT OUTER JOIN articles ON (\('default_scope' = 'default_scope'\) AND )?articles.person_id = people.id})
        expect(real_query)
                .to match(%r{LEFT OUTER JOIN articles articles_people ON (\('default_scope' = 'default_scope'\) AND )?articles_people.person_id = parents_people.id})

        expect(real_query)
          .to include "people.name = 'person_name_query'"
        expect(real_query)
          .to include "articles.title = 'person_article_title_query'"
        expect(real_query)
          .to include "parents_people.name = 'parent_name_query'"
        expect(real_query)
          .to include "articles_people.title = 'parents_article_title_query'"
      end

      it 'evaluates conditions for multiple `belongs_to` associations to the same table contextually' do
        s = Search.new(
          Recommendation,
          person_name_eq: 'Ernie',
          target_person_parent_name_eq: 'Test'
        ).result
        expect(s).to be_an ActiveRecord::Relation
        real_query = remove_quotes_and_backticks(s.to_sql)
        expected_query = <<-SQL
          SELECT recommendations.* FROM recommendations
          LEFT OUTER JOIN people ON people.id = recommendations.person_id
          LEFT OUTER JOIN people target_people_recommendations
            ON target_people_recommendations.id = recommendations.target_person_id
          LEFT OUTER JOIN people parents_people
            ON parents_people.id = target_people_recommendations.parent_id
          WHERE (people.name = 'Ernie' AND parents_people.name = 'Test')
        SQL
        .squish
        expect(real_query).to eq expected_query
      end

      it 'evaluates compound conditions contextually' do
        s = Search.new(Person, children_name_or_name_eq: 'Ernie').result
        expect(s).to be_an ActiveRecord::Relation
        expect(s.to_sql).to match /#{children_people_name_field
          } = 'Ernie' OR #{people_name_field} = 'Ernie'/
      end

      it 'evaluates polymorphic belongs_to association conditions contextually' do
        s = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie').result
        expect(s).to be_an ActiveRecord::Relation
        expect(s.to_sql).to match /#{people_name_field} = 'Ernie'/
        expect(s.to_sql).to match /#{notable_type_field} = 'Person'/
      end

      it 'evaluates nested conditions' do
        s = Search.new(Person, children_name_eq: 'Ernie',
          g: [
            { m: 'or', name_eq: 'Ernie', children_children_name_eq: 'Ernie' }
          ]
        ).result
        expect(s).to be_an ActiveRecord::Relation
        first, last = s.to_sql.split(/ AND /)
        expect(first).to match /#{children_people_name_field} = 'Ernie'/
        expect(last).to match /#{
          people_name_field} = 'Ernie' OR #{
          quote_table_name("children_people_2")}.#{
          quote_column_name("name")} = 'Ernie'/
      end

      it 'evaluates arrays of groupings' do
        s = Search.new(Person,
          g: [
            { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
            { m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' }
          ]
        ).result
        expect(s).to be_an ActiveRecord::Relation
        first, last = s.to_sql.split(/ AND /)
        expect(first).to match /#{people_name_field} = 'Ernie' OR #{
          children_people_name_field} = 'Ernie'/
        expect(last).to match /#{people_name_field} = 'Bert' OR #{
          children_people_name_field} = 'Bert'/
      end

      it 'returns distinct records when passed distinct: true' do
        s = Search.new(Person,
          g: [
            { m: 'or', comments_body_cont: 'e', articles_comments_body_cont: 'e' }
          ]
        )

        all_or_load, uniq_or_distinct = :load, :distinct
        expect(s.result.send(all_or_load).size)
        .to eq(9000)
        expect(s.result(distinct: true).size)
        .to eq(10)
        expect(s.result.send(all_or_load).send(uniq_or_distinct))
        .to eq s.result(distinct: true).send(all_or_load)
      end

      it 'evaluates joins with belongs_to join' do
        s = Person.joins(:parent).ransack(parent_name_eq: 'Ernie').result(distinct: true)
        expect(s).to be_an ActiveRecord::Relation
      end

      private

        def remove_quotes_and_backticks(str)
          str.gsub(/["`]/, '')
        end
    end

    describe '#sorts=' do
      before do
        @s = Search.new(Person)
      end

      it 'creates sorts based on a single attribute/direction' do
        @s.sorts = 'id desc'
        expect(@s.sorts.size).to eq(1)
        sort = @s.sorts.first
        expect(sort).to be_a Nodes::Sort
        expect(sort.name).to eq 'id'
        expect(sort.dir).to eq 'desc'
      end

      it 'creates sorts based on a single attribute and uppercase direction' do
        @s.sorts = 'id DESC'
        expect(@s.sorts.size).to eq(1)
        sort = @s.sorts.first
        expect(sort).to be_a Nodes::Sort
        expect(sort.name).to eq 'id'
        expect(sort.dir).to eq 'desc'
      end

      it 'creates sorts based on a single attribute and without direction' do
        @s.sorts = 'id'
        expect(@s.sorts.size).to eq(1)
        sort = @s.sorts.first
        expect(sort).to be_a Nodes::Sort
        expect(sort.name).to eq 'id'
        expect(sort.dir).to eq 'asc'
      end

      it 'creates sorts based on a single alias/direction' do
        @s.sorts = 'daddy desc'
        expect(@s.sorts.size).to eq(1)
        sort = @s.sorts.first
        expect(sort).to be_a Nodes::Sort
        expect(sort.name).to eq 'parent_name'
        expect(sort.dir).to eq 'desc'
      end

      it 'creates sorts based on a single alias and uppercase direction' do
        @s.sorts = 'daddy DESC'
        expect(@s.sorts.size).to eq(1)
        sort = @s.sorts.first
        expect(sort).to be_a Nodes::Sort
        expect(sort.name).to eq 'parent_name'
        expect(sort.dir).to eq 'desc'
      end

      it 'creates sorts based on a single alias and without direction' do
        @s.sorts = 'daddy'
        expect(@s.sorts.size).to eq(1)
        sort = @s.sorts.first
        expect(sort).to be_a Nodes::Sort
        expect(sort.name).to eq 'parent_name'
        expect(sort.dir).to eq 'asc'
      end

      it 'creates sorts based on attributes, alias and directions in array format' do
        @s.sorts = ['id desc', { name: 'daddy', dir: 'asc' }]
        expect(@s.sorts.size).to eq(2)
        sort1, sort2 = @s.sorts
        expect(sort1).to be_a Nodes::Sort
        expect(sort1.name).to eq 'id'
        expect(sort1.dir).to eq 'desc'
        expect(sort2).to be_a Nodes::Sort
        expect(sort2.name).to eq 'parent_name'
        expect(sort2.dir).to eq 'asc'
      end

      it 'creates sorts based on attributes, alias and uppercase directions in array format' do
        @s.sorts = ['id DESC', { name: 'daddy', dir: 'ASC' }]
        expect(@s.sorts.size).to eq(2)
        sort1, sort2 = @s.sorts
        expect(sort1).to be_a Nodes::Sort
        expect(sort1.name).to eq 'id'
        expect(sort1.dir).to eq 'desc'
        expect(sort2).to be_a Nodes::Sort
        expect(sort2.name).to eq 'parent_name'
        expect(sort2.dir).to eq 'asc'
      end

      it 'creates sorts based on attributes, alias and different directions
        in array format' do
        @s.sorts = ['id DESC', { name: 'daddy', dir: nil }]
        expect(@s.sorts.size).to eq(2)
        sort1, sort2 = @s.sorts
        expect(sort1).to be_a Nodes::Sort
        expect(sort1.name).to eq 'id'
        expect(sort1.dir).to eq 'desc'
        expect(sort2).to be_a Nodes::Sort
        expect(sort2.name).to eq 'parent_name'
        expect(sort2.dir).to eq 'asc'
      end

      it 'creates sorts based on attributes, alias and directions in hash format' do
        @s.sorts = {
          '0' => { name: 'id', dir: 'desc' },
          '1' => { name: 'daddy', dir: 'asc' }
        }
        expect(@s.sorts.size).to eq(2)
        expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
        id_sort = @s.sorts.detect { |s| s.name == 'id' }
        daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
        expect(id_sort.dir).to eq 'desc'
        expect(daddy_sort.dir).to eq 'asc'
      end

      it 'creates sorts based on attributes, alias and uppercase directions
        in hash format' do
        @s.sorts = {
          '0' => { name: 'id', dir: 'DESC' },
          '1' => { name: 'daddy', dir: 'ASC' }
        }
        expect(@s.sorts.size).to eq(2)
        expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
        id_sort = @s.sorts.detect { |s| s.name == 'id' }
        daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
        expect(id_sort.dir).to eq 'desc'
        expect(daddy_sort.dir).to eq 'asc'
      end

      it 'creates sorts based on attributes, alias and different directions
        in hash format' do
        @s.sorts = {
          '0' => { name: 'id', dir: 'DESC' },
          '1' => { name: 'daddy', dir: nil }
        }
        expect(@s.sorts.size).to eq(2)
        expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
        id_sort = @s.sorts.detect { |s| s.name == 'id' }
        daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
        expect(id_sort.dir).to eq 'desc'
        expect(daddy_sort.dir).to eq 'asc'
      end

      it 'overrides existing sort' do
        @s.sorts = 'id asc'
        expect(@s.result.first.id).to eq 1
      end

      it "PG's sort option", if: ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" do
        default = Ransack.options.clone

        s = Search.new(Person, s: 'name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" ASC"

        Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_first }
        s = Search.new(Person, s: 'name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" ASC NULLS FIRST"
        s = Search.new(Person, s: 'name desc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" DESC NULLS LAST"

        Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_last }
        s = Search.new(Person, s: 'name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" ASC NULLS LAST"
        s = Search.new(Person, s: 'name desc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" DESC NULLS FIRST"

        Ransack.options = default
      end

      it "PG's sort option with double name", if: ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" do
        default = Ransack.options.clone

        s = Search.new(Person, s: 'doubled_name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC"

        Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_first }
        s = Search.new(Person, s: 'doubled_name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS FIRST"
        s = Search.new(Person, s: 'doubled_name desc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS LAST"

        Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_last }
        s = Search.new(Person, s: 'doubled_name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS LAST"
        s = Search.new(Person, s: 'doubled_name desc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS FIRST"

        Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_always_first }
        s = Search.new(Person, s: 'doubled_name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS FIRST"
        s = Search.new(Person, s: 'doubled_name desc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS FIRST"

        Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_always_last }
        s = Search.new(Person, s: 'doubled_name asc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS LAST"
        s = Search.new(Person, s: 'doubled_name desc')
        expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS LAST"

        Ransack.options = default
      end
    end

    describe '#method_missing' do
      before do
        @s = Search.new(Person)
      end

      it 'raises NoMethodError when sent an invalid attribute' do
        expect { @s.blah }.to raise_error NoMethodError
      end

      it 'sets condition attributes when sent valid attributes' do
        @s.name_eq = 'Ernie'
        expect(@s.name_eq).to eq 'Ernie'
      end

      it 'allows chaining to access nested conditions' do
        @s.groupings = [
          { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' }
        ]
        expect(@s.groupings.first.children_name_eq).to eq 'Ernie'
      end
    end
  end
end