• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

drecom / activerecord-turntable / #1107

30 Aug 2023 05:56AM UTC coverage: 88.595% (+1.4%) from 87.21%
#1107

push

web-flow
Merge pull request #18 from akariiijima/add-ci-for-support-ar6.0.Z

Add CI for support-ar6.0.z

2649 of 2990 relevant lines covered (88.6%)

385.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.35
/lib/active_record/turntable/sql_tree_patch.rb
1
# rubocop:disable Style/CaseEquality
2
require "sql_tree"
1✔
3
require "active_support/core_ext/kernel/reporting"
1✔
4

5
module SQLTree
1✔
6
  class << self
1✔
7
    attr_accessor :identifier_quote_field_char
1✔
8
  end
9
  self.identifier_quote_field_char = "`"
1✔
10
end
11

12
class SQLTree::Token
1✔
13
  extended_keywords = %w(BINARY LIMIT OFFSET INDEX KEY USE FORCE IGNORE TRUE FALSE)
1✔
14
  KEYWORDS.concat(extended_keywords)
1✔
15

16
  extended_keywords.each do |kwd|
1✔
17
    const_set(kwd, Class.new(SQLTree::Token::Keyword))
10✔
18
  end
19

20
  BINARY_ESCAPE = Class.new(SQLTree::Token).new("x")
1✔
21

22
  def possible_index_hint?
1✔
23
    [SQLTree::Token::USE, SQLTree::Token::FORCE, SQLTree::Token::IGNORE].include?(self.class)
1,874✔
24
  end
25

26
  def index_keyword?
1✔
27
    [SQLTree::Token::INDEX, SQLTree::Token::KEY].include?(self.class)
12✔
28
  end
29
end
30

31
class SQLTree::Tokenizer
1✔
32
  def tokenize_quoted_string(&block) # :yields: SQLTree::Token::String
1✔
33
    string = ""
1,841✔
34
    until next_char.nil? || current_char == "'"
1,841✔
35
      string << (current_char == "\\" ? instance_eval("%@\\#{next_char.gsub('@', '\@')}@") : current_char)
33,614✔
36
    end
37
    handle_token(SQLTree::Token::String.new(string), &block)
1,841✔
38
  end
39

40
  # @note Override to handle x'..' binary string
41
  # rubocop:disable Lint/EmptyWhen:
42
  def each_token(&block) # :yields: SQLTree::Token
1✔
43
    while next_char
951✔
44
      case current_char
37,160✔
45
      when /^\s?$/ then # whitespace, go to next character
46
      when "(" then            handle_token(SQLTree::Token::LPAREN, &block)
1,666✔
47
      when ")" then            handle_token(SQLTree::Token::RPAREN, &block)
1,666✔
48
      when "." then            handle_token(SQLTree::Token::DOT, &block)
273✔
49
      when "," then            handle_token(SQLTree::Token::COMMA, &block)
6,839✔
50
      when /\d/ then           tokenize_number(&block)
2,657✔
51
      when "'" then            tokenize_quoted_string(&block)
1,841✔
52
      when "E", "x", "X" then  tokenize_possible_escaped_string(&block)
3✔
53
      when /\w/ then           tokenize_keyword(&block)
3,111✔
54
      when OPERATOR_CHARS then tokenize_operator(&block)
277✔
55
      when SQLTree.identifier_quote_char then tokenize_quoted_identifier(&block)
×
56
      when SQLTree.identifier_quote_field_char then tokenize_identifier_with_quote(&block)
5,620✔
57
      end
58
    end
59

60
    # Make sure to yield any tokens that are still stashed on the queue.
61
    empty_keyword_queue!(&block)
951✔
62
  end
63
  # rubocop:enable Lint/EmptyWhen:
64
  alias_method :each, :each_token
1✔
65

66
  def tokenize_possible_escaped_string(&block)
1✔
67
    if peek_char == "'"
3✔
68
      token = case current_char
3✔
69
              when "E"
70
                SQLTree::Token::STRING_ESCAPE
×
71
              when "x", "X"
72
                SQLTree::Token::BINARY_ESCAPE
3✔
73
              end
74
      handle_token(token, &block)
3✔
75
    else
76
      tokenize_keyword(&block)
×
77
    end
78
  end
79

80
  def tokenize_identifier_with_quote(&block)
1✔
81
    next_char # skip identifier_quote_field_char
5,620✔
82
    literal = current_char
5,620✔
83
    literal << next_char while SQLTree.identifier_quote_field_char != peek_char
5,620✔
84
    next_char # skip identifier_quote_field_char
5,620✔
85
    handle_token(SQLTree::Token::Identifier.new(literal), &block)
5,620✔
86
  end
87
end
88

89
module SQLTree::Node
1✔
90
  class Base
1✔
91
    def quote_field_name(field_name)
1✔
92
      "#{SQLTree.identifier_quote_field_char}#{field_name}#{SQLTree.identifier_quote_field_char}"
5,040✔
93
    end
94
  end
95

96
  class SelectQuery < Base
1✔
97
    child :offset
1✔
98

99
    def to_sql(options = {})
1✔
100
      raise "At least one SELECT expression is required" if self.select.empty?
6✔
101
      sql = self.distinct ? "SELECT DISTINCT " : "SELECT "
6✔
102
      sql << select.map { |s| s.to_sql(options) }.join(", ")
12✔
103
      sql << " FROM "     << from.map { |f| f.to_sql(options) }.join(", ") if from
11✔
104
      sql << " WHERE "    << where.to_sql(options) if where
6✔
105
      sql << " GROUP BY " << group_by.map { |g| g.to_sql(options) }.join(", ") if group_by
6✔
106
      sql << " ORDER BY " << order_by.map { |o| o.to_sql(options) }.join(", ") if order_by
6✔
107
      sql << " HAVING "   << having.to_sql(options) if having
6✔
108
      sql << " LIMIT "    << Array(limit).map { |f| f.to_sql(options) }.join(", ") if limit
6✔
109
      sql << " OFFSET "   << offset.to_sql(options) if offset
6✔
110
      sql
6✔
111
    end
112

113
    def self.parse(tokens)
1✔
114
      select_node = self.new
106✔
115
      tokens.consume(SQLTree::Token::SELECT)
106✔
116

117
      if SQLTree::Token::DISTINCT === tokens.peek
106✔
118
        tokens.consume(SQLTree::Token::DISTINCT)
×
119
        select_node.distinct = true
×
120
      end
121

122
      select_node.select   = parse_list(tokens, SQLTree::Node::SelectDeclaration)
106✔
123
      select_node.from     = self.parse_from_clause(tokens)   if SQLTree::Token::FROM === tokens.peek
106✔
124
      select_node.where    = self.parse_where_clause(tokens)  if SQLTree::Token::WHERE === tokens.peek
106✔
125
      if SQLTree::Token::GROUP === tokens.peek
106✔
126
        select_node.group_by = self.parse_group_clause(tokens)
×
127
        select_node.having   = self.parse_having_clause(tokens) if SQLTree::Token::HAVING === tokens.peek
×
128
      end
129
      select_node.order_by = self.parse_order_clause(tokens) if SQLTree::Token::ORDER === tokens.peek
106✔
130
      if SQLTree::Token::LIMIT === tokens.peek && (list = self.parse_limit_clause(tokens))
106✔
131
        select_node.offset = list.shift if list.size > 1
53✔
132
        select_node.limit  = list.shift
53✔
133
      end
134
      select_node.offset = self.parse_offset_clause(tokens) if SQLTree::Token::OFFSET === tokens.peek
106✔
135
      select_node
106✔
136
    end
137

138
    def self.parse_limit_clause(tokens)
1✔
139
      tokens.consume(SQLTree::Token::LIMIT)
53✔
140
      self.parse_list(tokens, SQLTree::Node::Expression)
53✔
141
    end
142

143
    def self.parse_offset_clause(tokens)
1✔
144
      tokens.consume(SQLTree::Token::OFFSET)
×
145
      Expression.parse(tokens)
×
146
    end
147
  end
148

149
  class SubQuery < SelectQuery
1✔
150
    def to_sql(options = {})
1✔
151
      "(" + super(options) + ")"
×
152
    end
153

154
    def self.parse(tokens)
1✔
155
      tokens.consume(SQLTree::Token::LPAREN)
×
156
      select_node = super(tokens)
×
157
      tokens.consume(SQLTree::Token::RPAREN)
×
158
      select_node
×
159
    end
160
  end
161

162
  class TableReference < Base
1✔
163
    leaf :index_hint
1✔
164

165
    def initialize(table, table_alias = nil, index_hint = nil)
1✔
166
      @table = table
950✔
167
      @table_alias = table_alias
950✔
168
      @index_hint = index_hint
950✔
169
    end
170

171
    def to_sql(options = {})
1✔
172
      sql = (SQLTree::Node::SubQuery === table) ? table.to_sql : quote_field_name(table)
821✔
173
      sql << " AS " << quote_field_name(table_alias) if table_alias
821✔
174
      sql << " " << index_hint.to_sql if index_hint
821✔
175
      sql
821✔
176
    end
177

178
    def self.parse(tokens)
1✔
179
      if SQLTree::Token::Identifier === tokens.peek
950✔
180
        tokens.next
950✔
181
        table_reference = self.new(tokens.current.literal)
950✔
182
        if tokens.peek && !tokens.peek.possible_index_hint? &&
950✔
183
           (SQLTree::Token::AS === tokens.peek || SQLTree::Token::Identifier === tokens.peek)
184
          tokens.consume(SQLTree::Token::AS) if SQLTree::Token::AS === tokens.peek
×
185
          table_reference.table_alias = tokens.next.literal
×
186
        end
187
        if tokens.peek && tokens.peek.possible_index_hint? && tokens.peek(2).index_keyword?
950✔
188
          table_reference.index_hint = SQLTree::Node::IndexHint.parse(tokens)
6✔
189
        end
190
        return table_reference
950✔
191
      elsif SQLTree::Token::SELECT === tokens.peek(2)
×
192
        table_reference = self.new(SQLTree::Node::SubQuery.parse(tokens))
×
193
        if SQLTree::Token::AS === tokens.peek || SQLTree::Token::Identifier === tokens.peek
×
194
          tokens.consume(SQLTree::Token::AS) if SQLTree::Token::AS === tokens.peek
×
195
          table_reference.table_alias = tokens.next.literal
×
196
        end
197
        table_reference
×
198
      else
199
        raise SQLTree::Parser::UnexpectedToken, tokens.current
×
200
      end
201
    end
202
  end
203

204
  class IndexHint < Base
1✔
205
    leaf :hint_method
1✔
206
    leaf :hint_key
1✔
207
    leaf :index_list
1✔
208

209
    def initialize(hint_method, hint_key, index_list)
1✔
210
      @hint_method = hint_method
6✔
211
      @hint_key = hint_key
6✔
212
      @index_list = index_list
6✔
213
    end
214

215
    def to_sql(options = {})
1✔
216
      sql = "#{hint_method} #{hint_key} "
3✔
217
      sql << "(#{index_list.map(&:to_sql).join(' ')})"
3✔
218
      sql
3✔
219
    end
220

221
    def self.parse(tokens)
1✔
222
      hint_method = tokens.next.literal
6✔
223
      if tokens.peek.index_keyword?
6✔
224
        hint_key = tokens.next.literal
6✔
225
        tokens.consume(SQLTree::Token::LPAREN)
6✔
226
        index_list = parse_list(tokens, SQLTree::Node::Expression::Field)
6✔
227
        tokens.consume(SQLTree::Token::RPAREN)
6✔
228
        self.new(hint_method, hint_key, index_list)
6✔
229
      else
230
        raise SQLTree::Parser::UnexpectedToken, tokens.current
×
231
      end
232
    end
233
  end
234

235
  class Expression < Base
1✔
236
    class BinaryOperator < SQLTree::Node::Expression
1✔
237
      TOKEN_PRECEDENCE[2] << SQLTree::Token::BETWEEN
1✔
238
      silence_warnings do
1✔
239
        TOKENS = TOKEN_PRECEDENCE.flatten
1✔
240
      end
241

242
      def self.parse_rhs(tokens, precedence, operator = nil)
1✔
243
        if ["IN", "NOT IN"].include?(operator)
203✔
244
          if SQLTree::Token::SELECT === tokens.peek(2)
7✔
245
            return SQLTree::Node::SubQuery.parse(tokens)
×
246
          else
247
            return List.parse(tokens)
7✔
248
          end
249
        elsif ["IS", "IS NOT"].include?(operator)
196✔
250
          tokens.consume(SQLTree::Token::NULL)
×
251
          return SQLTree::Node::Expression::Value.new(nil)
×
252
        elsif ["BETWEEN"].include?(operator)
196✔
253
          expr = parse_atomic(tokens)
×
254
          operator = parse_operator(tokens)
×
255
          rhs      = parse_rhs(tokens, precedence, operator)
×
256
          expr     = self.new(operator: operator, lhs: expr, rhs: rhs)
×
257
          return expr
×
258
        else
259
          return parse(tokens, precedence + 1)
196✔
260
        end
261
      end
262
    end
263

264
    class PrefixOperator < SQLTree::Node::Expression
1✔
265
      TOKENS << SQLTree::Token::BINARY
1✔
266
    end
267

268
    class Field < Variable
1✔
269
      def to_sql(options = {})
1✔
270
        @table.nil? ? quote_field_name(@name) : quote_field_name(@table) + "." + quote_field_name(@name)
4,217✔
271
      end
272
    end
273

274
    class Value
1✔
275
      leaf :escape
1✔
276

277
      def to_sql(options = {})
1✔
278
        case value
7✔
279
        when nil;            'NULL'
×
280
        when true;           'TRUE'
×
281
        when false;          'FALSE'
×
282
        when String;         quote_str(@value)
3✔
283
        when Numeric;        @value.to_s
4✔
284
        when Date;           @value.strftime("'%Y-%m-%d'")
×
285
        when DateTime, Time; @value.strftime("'%Y-%m-%d %H:%M:%S'")
×
286
        else raise "Don't know how te represent this value in SQL!"
×
287
        end
288
      end
289

290
      def self.parse(tokens)
1✔
291
        case tokens.next
4,501✔
292
        when SQLTree::Token::String, SQLTree::Token::Number
293
          SQLTree::Node::Expression::Value.new(tokens.current.literal)
4,495✔
294
        when SQLTree::Token::NULL
295
          SQLTree::Node::Expression::Value.new(nil)
6✔
296
        when SQLTree::Token::TRUE
297
          SQLTree::Node::Expression::Value.new(true)
×
298
        when SQLTree::Token::FALSE
299
          SQLTree::Node::Expression::Value.new(false)
×
300
        else
301
          raise SQLTree::Parser::UnexpectedToken.new(tokens.current, :literal)
×
302
        end
303
      end
304
    end
305

306
    class EscapedValue < Value
1✔
307
      def initialize(value, escape = nil)
1✔
308
        @value = value
3✔
309
        @escape = escape
3✔
310
      end
311

312
      def to_sql(options = {})
1✔
313
        case value
1✔
314
        when nil then            "NULL"
×
315
        when String then         "#{escape_string}#{quote_str(@value)}"
1✔
316
        when Numeric then        @value.to_s
×
317
        when Date then           @value.strftime("'%Y-%m-%d'")
×
318
        when DateTime, Time then @value.strftime("'%Y-%m-%d %H:%M:%S'")
×
319
        else raise "Don't know how te represent this value in SQL!"
×
320
        end
321
      end
322

323
      def escape_string
1✔
324
        @escape.to_s
1✔
325
      end
326

327
      def self.parse(tokens)
1✔
328
        escape = tokens.next
3✔
329
        case tokens.next
3✔
330
        when SQLTree::Token::String
331
          SQLTree::Node::Expression::EscapedValue.new(tokens.current.literal, escape.literal)
3✔
332
        else
333
          raise SQLTree::Parser::UnexpectedToken.new(tokens.current, :literal)
×
334
        end
335
      end
336
    end
337

338
    def self.parse_atomic(tokens)
1✔
339
      if SQLTree::Token::LPAREN === tokens.peek
4,697✔
340
        tokens.consume(SQLTree::Token::LPAREN)
×
341
        expr = self.parse(tokens)
×
342
        tokens.consume(SQLTree::Token::RPAREN)
×
343
        expr
×
344
      elsif tokens.peek.prefix_operator?
4,697✔
345
        PrefixOperator.parse(tokens)
×
346
      elsif tokens.peek.variable?
4,697✔
347
        if SQLTree::Token::LPAREN === tokens.peek(2)
193✔
348
          FunctionCall.parse(tokens)
×
349
        elsif SQLTree::Token::DOT === tokens.peek(2)
193✔
350
          Field.parse(tokens)
183✔
351
        else
352
          Variable.parse(tokens)
10✔
353
        end
354
      elsif SQLTree::Token::STRING_ESCAPE == tokens.peek
4,504✔
355
        EscapedValue.parse(tokens)
×
356
      elsif SQLTree::Token::BINARY_ESCAPE == tokens.peek
4,504✔
357
        EscapedValue.parse(tokens)
3✔
358
      elsif SQLTree::Token::INTERVAL === tokens.peek
4,501✔
359
        IntervalValue.parse(tokens)
×
360
      else
361
        Value.parse(tokens)
4,501✔
362
      end
363
    end
364
  end
365

366
  class InsertQuery < Base
1✔
367
    def to_sql(options = {})
1✔
368
      sql = "INSERT INTO #{table.to_sql(options)} "
816✔
369
      sql << "(" + fields.map { |f| f.to_sql(options) }.join(", ") + ") " if fields
5,028✔
370
      sql << "VALUES"
816✔
371
      sql << values.map do |value|
816✔
372
        " (" + value.map { |v| v.to_sql(options) }.join(", ") + ")"
1,632✔
373
      end.join(",")
374
      sql
816✔
375
    end
376

377
    def self.parse_value_list(tokens)
1✔
378
      values = []
820✔
379
      tokens.consume(SQLTree::Token::VALUES)
820✔
380
      tokens.consume(SQLTree::Token::LPAREN)
820✔
381
      values << parse_list(tokens)
820✔
382
      tokens.consume(SQLTree::Token::RPAREN)
820✔
383
      while SQLTree::Token::COMMA === tokens.peek
820✔
384
        tokens.consume(SQLTree::Token::COMMA)
9✔
385
        tokens.consume(SQLTree::Token::LPAREN)
9✔
386
        values << parse_list(tokens)
9✔
387
        tokens.consume(SQLTree::Token::RPAREN)
9✔
388
      end
389
      values
820✔
390
    end
391
  end
392
end
393
# rubocop:enable Style/CaseEquality
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc