order_query 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGES.md +5 -0
- data/README.md +1 -1
- data/lib/order_query/column.rb +17 -14
- data/lib/order_query/direction.rb +32 -0
- data/lib/order_query/sql/order_by.rb +28 -24
- data/lib/order_query/sql/where.rb +4 -7
- data/lib/order_query/version.rb +1 -1
- data/spec/order_query_spec.rb +37 -1
- metadata +3 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 6e69a0283a9dcc95fe70d2fd18c02f5aa62120d2
         | 
| 4 | 
            +
              data.tar.gz: d1d0a0975074175ff4eb15d7fa5efdd20fc91c3f
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 0a5d6a00cd9096587584e9c32a1daf05f26577175d9e0bf6a783b8bc189a710482143b18166b82620b3ef315928c51ed837f6776e719ff53c53c644cfc278fa6
         | 
| 7 | 
            +
              data.tar.gz: 6ae64249791c2c8ac500b10b3a50c9ed6cf439056301ca1d361f1d8f5559fa1180bcdf72113698922a08ad6fd85f5e3adcd3c25c5b9e9f50776225ce51c616f2
         | 
    
        data/CHANGES.md
    CHANGED
    
    | @@ -1,3 +1,8 @@ | |
| 1 | 
            +
            ## 0.3.2
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            * Optimization: do not wrap top-level disjunctive in `AND` when the column has an enumerated order. [Read more](https://github.com/glebm/order_query/issues/3#issuecomment-54764638).
         | 
| 4 | 
            +
            * Boolean enum columns (e.g. `[:pinned, [true, false]]`) are now automatically collapsed to `ORDER by column ASC|DESC`.
         | 
| 5 | 
            +
             | 
| 1 6 | 
             
            ## 0.3.1
         | 
| 2 7 |  | 
| 3 8 | 
             
            * Automatically add primary key when there is no unique column for the order
         | 
    
        data/README.md
    CHANGED
    
    
    
        data/lib/order_query/column.rb
    CHANGED
    
    | @@ -1,29 +1,32 @@ | |
| 1 1 | 
             
            # coding: utf-8
         | 
| 2 | 
            +
            require 'order_query/direction'
         | 
| 2 3 | 
             
            require 'order_query/sql/column'
         | 
| 3 4 | 
             
            module OrderQuery
         | 
| 4 5 | 
             
              # An order column (sort column)
         | 
| 5 6 | 
             
              class Column
         | 
| 6 | 
            -
                attr_reader :name, : | 
| 7 | 
            +
                attr_reader :name, :order_enum, :options
         | 
| 7 8 | 
             
                delegate :column_name, :quote, to: :@sql
         | 
| 8 9 |  | 
| 9 10 | 
             
                # @option spec [String] :unique    Mark the attribute as unique to avoid redundant columns
         | 
| 10 11 | 
             
                def initialize(spec, scope)
         | 
| 11 | 
            -
                  spec | 
| 12 | 
            -
                  options | 
| 13 | 
            -
                  @name | 
| 14 | 
            -
                   | 
| 15 | 
            -
                     | 
| 16 | 
            -
             | 
| 17 | 
            -
                      @order      = spec[2] || :desc
         | 
| 18 | 
            -
                    else
         | 
| 19 | 
            -
                      @order = spec[1] || :asc
         | 
| 12 | 
            +
                  spec       = spec.dup
         | 
| 13 | 
            +
                  options    = spec.extract_options!
         | 
| 14 | 
            +
                  @name      = spec[0]
         | 
| 15 | 
            +
                  if spec[1].is_a?(Array)
         | 
| 16 | 
            +
                    @order_enum = spec.delete_at(1)
         | 
| 17 | 
            +
                    spec[1] ||= :desc
         | 
| 20 18 | 
             
                  end
         | 
| 21 | 
            -
                  @ | 
| 19 | 
            +
                  @direction = Direction.parse!(spec[1] || :asc)
         | 
| 20 | 
            +
                  @options   = options.reverse_merge(
         | 
| 22 21 | 
             
                      unique:   name.to_s == scope.primary_key,
         | 
| 23 22 | 
             
                      complete: true
         | 
| 24 23 | 
             
                  )
         | 
| 25 | 
            -
                  @unique | 
| 26 | 
            -
                  @sql | 
| 24 | 
            +
                  @unique    = @options[:unique]
         | 
| 25 | 
            +
                  @sql       = SQL::Column.new(self, scope)
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def direction(reverse = false)
         | 
| 29 | 
            +
                  reverse ? Direction.reverse(@direction) : @direction
         | 
| 27 30 | 
             
                end
         | 
| 28 31 |  | 
| 29 32 | 
             
                def unique?
         | 
| @@ -40,7 +43,7 @@ module OrderQuery | |
| 40 43 | 
             
                  ord = order_enum
         | 
| 41 44 | 
             
                  pos = ord.index(value)
         | 
| 42 45 | 
             
                  if pos
         | 
| 43 | 
            -
                    dir =  | 
| 46 | 
            +
                    dir = direction
         | 
| 44 47 | 
             
                    if side == :after && dir == :desc || side == :before && dir == :asc
         | 
| 45 48 | 
             
                      ord.from pos + (strict ? 1 : 0)
         | 
| 46 49 | 
             
                    else
         | 
| @@ -0,0 +1,32 @@ | |
| 1 | 
            +
            module OrderQuery
         | 
| 2 | 
            +
              # Responsible for handling :asc and :desc
         | 
| 3 | 
            +
              module Direction
         | 
| 4 | 
            +
                extend self
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                DIRECTIONS   = [:asc, :desc].freeze
         | 
| 7 | 
            +
                DIRECTIONS_S = DIRECTIONS.map { |d| d.to_s.freeze }.freeze
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def all
         | 
| 10 | 
            +
                  DIRECTIONS
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                # @param [:asc, :desc] direction
         | 
| 14 | 
            +
                # @return [:asc, :desc]
         | 
| 15 | 
            +
                def reverse(direction)
         | 
| 16 | 
            +
                  all[(all.index(direction) + 1) % 2].to_sym
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                # @param [:asc, :desc, String] direction
         | 
| 20 | 
            +
                # @raise [ArgumentError]
         | 
| 21 | 
            +
                # @return [:asc, :desc]
         | 
| 22 | 
            +
                def parse!(direction)
         | 
| 23 | 
            +
                  if direction.is_a?(Symbol)
         | 
| 24 | 
            +
                    direction if DIRECTIONS.include?(direction)
         | 
| 25 | 
            +
                  else
         | 
| 26 | 
            +
                    direction = direction.to_s.downcase
         | 
| 27 | 
            +
                    direction.to_sym if DIRECTIONS_S.include?(direction)
         | 
| 28 | 
            +
                  end or
         | 
| 29 | 
            +
                      raise ArgumentError.new("sort direction must be in #{all.map(&:inspect).join(', ')}, is #{direction.inspect}")
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
            end
         | 
| @@ -13,48 +13,52 @@ module OrderQuery | |
| 13 13 |  | 
| 14 14 | 
             
                  # @return [String]
         | 
| 15 15 | 
             
                  def build_reverse
         | 
| 16 | 
            -
                    @reverse_sql ||= join_order_by_clauses  | 
| 16 | 
            +
                    @reverse_sql ||= join_order_by_clauses order_by_sql_clauses(true)
         | 
| 17 17 | 
             
                  end
         | 
| 18 18 |  | 
| 19 19 | 
             
                  protected
         | 
| 20 20 |  | 
| 21 21 | 
             
                  # @return [Array<String>]
         | 
| 22 | 
            -
                  def order_by_sql_clauses
         | 
| 23 | 
            -
                    @columns.map { | | 
| 22 | 
            +
                  def order_by_sql_clauses(reverse = false)
         | 
| 23 | 
            +
                    @columns.map { |col| column_clause col, reverse }
         | 
| 24 24 | 
             
                  end
         | 
| 25 25 |  | 
| 26 | 
            -
                  def column_clause( | 
| 27 | 
            -
                     | 
| 28 | 
            -
             | 
| 29 | 
            -
                    if cond.order_enum
         | 
| 30 | 
            -
                      cond.order_enum.map { |v| "#{col_sql}=#{cond.quote v} #{dir_sql}" }.join(', ').freeze
         | 
| 26 | 
            +
                  def column_clause(col, reverse = false)
         | 
| 27 | 
            +
                    if col.order_enum
         | 
| 28 | 
            +
                      column_clause_enum col, reverse
         | 
| 31 29 | 
             
                    else
         | 
| 32 | 
            -
                       | 
| 30 | 
            +
                      column_clause_ray col, reverse
         | 
| 33 31 | 
             
                    end
         | 
| 34 32 | 
             
                  end
         | 
| 35 33 |  | 
| 36 | 
            -
                   | 
| 37 | 
            -
             | 
| 38 | 
            -
                   | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
                     | 
| 42 | 
            -
             | 
| 34 | 
            +
                  def column_clause_ray(col, reverse = false)
         | 
| 35 | 
            +
                    "#{col.column_name} #{sort_direction_sql(col, reverse)}".freeze
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  def column_clause_enum(col, reverse = false)
         | 
| 39 | 
            +
                    enum = col.order_enum
         | 
| 40 | 
            +
                    # Collapse boolean enum to `ORDER BY column ASC|DESC`
         | 
| 41 | 
            +
                    if enum == [false, true] || enum == [true, false]
         | 
| 42 | 
            +
                      return column_clause_ray col, reverse ^ enum.last
         | 
| 43 43 | 
             
                    end
         | 
| 44 | 
            +
                    enum.map { |v|
         | 
| 45 | 
            +
                      "#{order_by_value_sql col, v} #{sort_direction_sql(col, reverse)}"
         | 
| 46 | 
            +
                    }.join(', ').freeze
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def order_by_value_sql(col, v)
         | 
| 50 | 
            +
                    "#{col.column_name}=#{col.quote v}"
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  # @return [String]
         | 
| 54 | 
            +
                  def sort_direction_sql(col, reverse = false)
         | 
| 55 | 
            +
                    col.direction(reverse).to_s.upcase.freeze
         | 
| 44 56 | 
             
                  end
         | 
| 45 57 |  | 
| 46 58 | 
             
                  # @param [Array<String>] clauses
         | 
| 47 59 | 
             
                  def join_order_by_clauses(clauses)
         | 
| 48 60 | 
             
                    clauses.join(', ').freeze
         | 
| 49 61 | 
             
                  end
         | 
| 50 | 
            -
             | 
| 51 | 
            -
                  # @return [Array<String>]
         | 
| 52 | 
            -
                  def order_by_reverse_sql_clauses
         | 
| 53 | 
            -
                    swap = {'DESC' => 'ASC', 'ASC' => 'DESC'}
         | 
| 54 | 
            -
                    order_by_sql_clauses.map { |s|
         | 
| 55 | 
            -
                      s.gsub(/DESC|ASC/) { |m| swap[m] }
         | 
| 56 | 
            -
                    }
         | 
| 57 | 
            -
                  end
         | 
| 58 62 | 
             
                end
         | 
| 59 63 | 
             
              end
         | 
| 60 64 | 
             
            end
         | 
| @@ -65,9 +65,8 @@ module OrderQuery | |
| 65 65 | 
             
                  # Read more at https://github.com/glebm/order_query/issues/3
         | 
| 66 66 | 
             
                  def wrap_top_level_or(query, terms, side)
         | 
| 67 67 | 
             
                    top_term_i = terms.index(&:present?)
         | 
| 68 | 
            -
                    if top_term_i && terms[top_term_i].length == 2 &&
         | 
| 69 | 
            -
             | 
| 70 | 
            -
                      join_terms 'AND'.freeze, cond, wrap_term_with_parens(query)
         | 
| 68 | 
            +
                    if top_term_i && terms[top_term_i].length == 2 && !(col = @columns[top_term_i]).order_enum
         | 
| 69 | 
            +
                      join_terms 'AND'.freeze, where_side(col, side, false), wrap_term_with_parens(query)
         | 
| 71 70 | 
             
                    else
         | 
| 72 71 | 
             
                      query
         | 
| 73 72 | 
             
                    end
         | 
| @@ -107,11 +106,9 @@ module OrderQuery | |
| 107 106 | 
             
                    [%Q(#{col.column_name} = ?).freeze, [value]]
         | 
| 108 107 | 
             
                  end
         | 
| 109 108 |  | 
| 109 | 
            +
                  RAY_OP = {asc: '>'.freeze, desc: '<'.freeze}.freeze
         | 
| 110 110 | 
             
                  def where_ray(col, from, mode, strict = true)
         | 
| 111 | 
            -
                     | 
| 112 | 
            -
                    ops = ops.reverse if mode == :after
         | 
| 113 | 
            -
                    op  = {asc: ops[0], desc: ops[1]}[col.order || :asc]
         | 
| 114 | 
            -
                    ["#{col.column_name} #{op}#{'=' unless strict} ?".freeze, [from]]
         | 
| 111 | 
            +
                    ["#{col.column_name} #{RAY_OP[col.direction(mode == :before)]}#{'=' unless strict} ?".freeze, [from]]
         | 
| 115 112 | 
             
                  end
         | 
| 116 113 |  | 
| 117 114 | 
             
                  WHERE_IDENTITY = [''.freeze, [].freeze].freeze
         | 
    
        data/lib/order_query/version.rb
    CHANGED
    
    
    
        data/spec/order_query_spec.rb
    CHANGED
    
    | @@ -57,7 +57,7 @@ end | |
| 57 57 | 
             
            describe 'OrderQuery' do
         | 
| 58 58 |  | 
| 59 59 | 
             
              [false, true].each do |wrap_top_level_or|
         | 
| 60 | 
            -
                context "( | 
| 60 | 
            +
                context "(wtlo: #{wrap_top_level_or})" do
         | 
| 61 61 | 
             
                  wrap_top_level_or wrap_top_level_or
         | 
| 62 62 |  | 
| 63 63 | 
             
                  context 'Issue test model' do
         | 
| @@ -210,6 +210,42 @@ describe 'OrderQuery' do | |
| 210 210 | 
             
                      expect(o2.next(true)).to eq(p1)
         | 
| 211 211 | 
             
                    end
         | 
| 212 212 |  | 
| 213 | 
            +
                    context 'boolean enum order' do
         | 
| 214 | 
            +
                      before do
         | 
| 215 | 
            +
                        create_post pinned: true
         | 
| 216 | 
            +
                        create_post pinned: false
         | 
| 217 | 
            +
                      end
         | 
| 218 | 
            +
                      after do
         | 
| 219 | 
            +
                        Post.delete_all
         | 
| 220 | 
            +
                      end
         | 
| 221 | 
            +
                      it 'ORDER BY is collapsed' do
         | 
| 222 | 
            +
                        expect(Post.seek([:pinned, [true, false]]).scope.to_sql).to include('ORDER BY "posts"."pinned" DESC')
         | 
| 223 | 
            +
                      end
         | 
| 224 | 
            +
                      it 'enum asc' do
         | 
| 225 | 
            +
                        expect(Post.seek([:pinned, [false, true], :asc]).scope.pluck(:pinned)).to eq([true, false])
         | 
| 226 | 
            +
                        expect(Post.seek([:pinned, [true, false], :asc]).scope.pluck(:pinned)).to eq([false, true])
         | 
| 227 | 
            +
                      end
         | 
| 228 | 
            +
                      it 'enum desc' do
         | 
| 229 | 
            +
                        expect(Post.seek([:pinned, [false, true], :desc]).scope.pluck(:pinned)).to eq([false, true])
         | 
| 230 | 
            +
                        expect(Post.seek([:pinned, [true, false], :desc]).scope.pluck(:pinned)).to eq([true, false])
         | 
| 231 | 
            +
                      end
         | 
| 232 | 
            +
                    end
         | 
| 233 | 
            +
             | 
| 234 | 
            +
                    xcontext 'nil in enum' do
         | 
| 235 | 
            +
                      states = [nil, false, true]
         | 
| 236 | 
            +
                      let!(:posts) { states.map { |state| create_post(pinned: state) } }
         | 
| 237 | 
            +
                      states.permutation do |p|
         | 
| 238 | 
            +
                        # There is no cross-DB SQL that can be generated to position nil results
         | 
| 239 | 
            +
                        # http://use-the-index-luke.com/sql/sorting-grouping/order-by-asc-desc-nulls-last
         | 
| 240 | 
            +
                        next unless p.first.nil? || p.last.nil?
         | 
| 241 | 
            +
                        # Positioning NULLs first or last can be achieved, but remains on the ToDo / contributions welcome list
         | 
| 242 | 
            +
                        it "nil in enum works for #{p}" do
         | 
| 243 | 
            +
                          expect(Post.seek([:pinned, p]).scope.all.map(&:pinned)).to eq(p)
         | 
| 244 | 
            +
                          expect(Post.seek([:pinned, p, :asc]).scope.all.map(&:pinned)).to eq(p.reverse)
         | 
| 245 | 
            +
                        end
         | 
| 246 | 
            +
                      end
         | 
| 247 | 
            +
                    end
         | 
| 248 | 
            +
             | 
| 213 249 | 
             
                    before do
         | 
| 214 250 | 
             
                      Post.delete_all
         | 
| 215 251 | 
             
                    end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: order_query
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.3. | 
| 4 | 
            +
              version: 0.3.2
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Gleb Mazovetskiy
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2014-09- | 
| 11 | 
            +
            date: 2014-09-15 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: activerecord
         | 
| @@ -79,6 +79,7 @@ files: | |
| 79 79 | 
             
            - Rakefile
         | 
| 80 80 | 
             
            - lib/order_query.rb
         | 
| 81 81 | 
             
            - lib/order_query/column.rb
         | 
| 82 | 
            +
            - lib/order_query/direction.rb
         | 
| 82 83 | 
             
            - lib/order_query/point.rb
         | 
| 83 84 | 
             
            - lib/order_query/space.rb
         | 
| 84 85 | 
             
            - lib/order_query/sql/column.rb
         |