File: select.rb

package info (click to toggle)
ruby-riddle 2.3.1-2~deb10u1
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 10,752 kB
  • sloc: sql: 25,022; php: 5,992; ruby: 4,757; sh: 59; makefile: 5
file content (245 lines) | stat: -rw-r--r-- 5,474 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# frozen_string_literal: true

class Riddle::Query::Select
  def initialize
    @values                = []
    @indices               = []
    @matching              = nil
    @wheres                = {}
    @where_alls            = {}
    @where_nots            = {}
    @where_not_alls        = {}
    @group_by              = nil
    @group_best            = nil
    @having                = []
    @order_by              = nil
    @order_within_group_by = nil
    @offset                = nil
    @limit                 = nil
    @options               = {}
  end

  def values(*values)
    @values += values
    self
  end

  def prepend_values(*values)
    @values.insert 0, *values
    self
  end

  def from(*indices)
    @indices += indices
    self
  end

  def matching(match)
    @matching = match
    self
  end

  def where(filters = {})
    @wheres.merge!(filters)
    self
  end

  def where_all(filters = {})
    @where_alls.merge!(filters)
    self
  end

  def where_not(filters = {})
    @where_nots.merge!(filters)
    self
  end

  def where_not_all(filters = {})
    @where_not_alls.merge!(filters)
    self
  end

  def group_by(attribute)
    @group_by = attribute
    self
  end

  def group_best(count)
    @group_best = count
    self
  end

  def having(*conditions)
    @having += conditions
    self
  end

  def order_by(order)
    @order_by = order
    self
  end

  def order_within_group_by(order)
    @order_within_group_by = order
    self
  end

  def limit(limit)
    @limit = limit
    self
  end

  def offset(offset)
    @offset = offset
    self
  end

  def with_options(options = {})
    @options.merge! options
    self
  end

  def to_sql
    sql = StringIO.new String.new(""), "w"
    sql << "SELECT #{ extended_values } FROM #{ @indices.join(', ') }"
    sql << " WHERE #{ combined_wheres }" if wheres?
    sql << " #{group_prefix} #{escape_columns(@group_by)}" if !@group_by.nil?
    unless @order_within_group_by.nil?
      sql << " WITHIN GROUP ORDER BY #{escape_columns(@order_within_group_by)}"
    end
    sql << " HAVING #{@having.join(' AND ')}" unless @having.empty?
    sql << " ORDER BY #{escape_columns(@order_by)}" if !@order_by.nil?
    sql << " #{limit_clause}"   unless @limit.nil? && @offset.nil?
    sql << " #{options_clause}" unless @options.empty?

    sql.string
  end

  private

  def extended_values
    @values.empty? ? '*' : @values.join(', ')
  end

  def group_prefix
    ['GROUP', @group_best, 'BY'].compact.join(' ')
  end

  def wheres?
    !(@wheres.empty? && @where_alls.empty? && @where_nots.empty? && @where_not_alls.empty? && @matching.nil?)
  end

  def combined_wheres
    wheres = wheres_to_s

    if @matching.nil?
      wheres
    elsif wheres.empty?
      "MATCH(#{Riddle::Query.quote @matching})"
    else
      "MATCH(#{Riddle::Query.quote @matching}) AND #{wheres}"
    end
  end

  def wheres_to_s
    (
      @wheres.keys.collect { |key|
        filter_comparison_and_value key, @wheres[key]
      } +
      @where_alls.collect { |key, values|
        values.collect { |value|
          filter_comparison_and_value key, value
        }
      } +
      @where_nots.keys.collect { |key|
        exclusive_filter_comparison_and_value key, @where_nots[key]
      } +
      @where_not_alls.collect { |key, values|
        '(' + values.collect { |value|
          exclusive_filter_comparison_and_value key, value
        }.join(' OR ') + ')'
      }
    ).flatten.compact.join(' AND ')
  end

  def filter_comparison_and_value(attribute, value)
    case value
    when Array
      if !value.flatten.empty?
        "#{escape_column(attribute)} IN (#{value.collect { |val| filter_value(val) }.join(', ')})"
      end
    when Range
      "#{escape_column(attribute)} BETWEEN #{filter_value(value.first)} AND #{filter_value(value.last)}"
    else
      "#{escape_column(attribute)} = #{filter_value(value)}"
    end
  end

  def exclusive_filter_comparison_and_value(attribute, value)
    case value
    when Array
      if !value.flatten.empty?
        "#{escape_column(attribute)} NOT IN (#{value.collect { |val| filter_value(val) }.join(', ')})"
      end
    when Range
      "#{escape_column(attribute)} < #{filter_value(value.first)} OR #{attribute} > #{filter_value(value.last)}"
    else
      "#{escape_column(attribute)} <> #{filter_value(value)}"
    end
  end

  def filter_value(value)
    case value
    when TrueClass
      1
    when FalseClass
      0
    when Time
      value.to_i
    when Date
      Time.utc(value.year, value.month, value.day).to_i
    when String
      "'#{value.gsub("'", "\\'")}'"
    else
      value
    end
  end

  def limit_clause
    if @offset.nil?
      "LIMIT #{@limit}"
    else
      "LIMIT #{@offset}, #{@limit || 20}"
    end
  end

  def options_clause
    'OPTION ' + @options.keys.collect { |key|
      "#{key}=#{option_value @options[key]}"
    }.join(', ')
  end

  def option_value(value)
    case value
    when Hash
      '(' + value.collect { |key, value| "#{key}=#{value}" }.join(', ') + ')'
    else
      value
    end
  end

  def escape_column(column)
    if column.to_s[/\A[`@]/] || column.to_s[/\A\w+\(/] || column.to_s[/\A\w+[.\[]/]
      column
    else
      column_name, *extra = column.to_s.split(' ')
      extra.unshift("`#{column_name}`").compact.join(' ')
    end
  end

  def escape_columns(columns)
    columns.to_s.split(/,\s*/).collect { |column|
      escape_column(column)
    }.join(', ')
  end
end