skylight 1.0.0.beta4 → 1.0.0.beta5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,3 @@
1
- require "sql_lexer"
2
1
  require "json"
3
2
 
4
3
  module Skylight
@@ -41,24 +40,8 @@ module Skylight
41
40
  private
42
41
 
43
42
  def extract_binds(payload, precalculated)
44
- case config[:sql_mode]
45
- when 'rust'.freeze
46
- extract_rust(payload)
47
- when 'ruby'.freeze
48
- extract_ruby(payload, precalculated)
49
- else
50
- raise "Unrecognized sql_mode: #{config.sql_mode}"
51
- end
52
- end
53
-
54
- def extract_rust(payload)
55
43
  Skylight.lex_sql(payload[:sql])
56
44
  end
57
-
58
- def extract_ruby(payload, precalculated)
59
- name, title, _ = SqlLexer::Lexer.bindify(payload[:sql], precalculated, true)
60
- [ name, title ]
61
- end
62
45
  end
63
46
  end
64
47
  end
@@ -12,7 +12,7 @@ module Skylight
12
12
 
13
13
  def get_method(endpoint)
14
14
  method = endpoint.options[:method].first
15
- method << "..." if endpoint.options[:method].length > 1
15
+ method = "#{method}..." if endpoint.options[:method].length > 1
16
16
  method
17
17
  end
18
18
 
@@ -8,11 +8,18 @@ module Skylight
8
8
  alias append_info_to_payload_without_sk append_info_to_payload
9
9
  def append_info_to_payload(payload)
10
10
  append_info_to_payload_without_sk(payload)
11
+ if respond_to?(:rendered_format)
12
+ rendered_mime = rendered_format
13
+ else
14
+ format = lookup_context.formats.first
15
+ rendered_mime = Mime[format.to_sym] if format
16
+ end
17
+ payload[:rendered_format] = rendered_mime.try(:ref)
11
18
  payload[:variant] = request.respond_to?(:variant) ? request.variant : nil
12
19
  end
13
20
  end
14
21
 
15
- #if RAILS_VERSION < 4.2.1
22
+ if Gem::Version.new(Rails.version) < Gem::Version.new('4.2.1')
16
23
  # Backport https://github.com/rails/rails/pull/17978
17
24
  ::ActionController::Instrumentation.class_eval do
18
25
  def process_action(*args)
@@ -37,7 +44,7 @@ module Skylight
37
44
  end
38
45
  end
39
46
  end
40
- #end
47
+ end
41
48
  end
42
49
  end
43
50
  end
@@ -5,11 +5,14 @@ module Skylight
5
5
  class Probe
6
6
  def install
7
7
  require 'sequel/database/logging'
8
- ::Sequel::Database.class_eval do
9
- alias log_yield_without_sk log_yield
10
8
 
11
- def log_yield(sql, args=nil, &block)
12
- log_yield_without_sk(sql, *args) do
9
+ method_name = ::Sequel::Database.method_defined?(:log_connection_yield) ? 'log_connection_yield' : 'log_yield'
10
+
11
+ ::Sequel::Database.class_eval <<-end_eval
12
+ alias #{method_name}_without_sk #{method_name}
13
+
14
+ def #{method_name}(sql, *args, &block)
15
+ #{method_name}_without_sk(sql, *args) do
13
16
  ::ActiveSupport::Notifications.instrument(
14
17
  "sql.sequel",
15
18
  sql: sql,
@@ -20,7 +23,7 @@ module Skylight
20
23
  end
21
24
  end
22
25
  end
23
- end
26
+ end_eval
24
27
  end
25
28
  end
26
29
  end
@@ -8,7 +8,9 @@ module Skylight
8
8
  alias compile_without_sk! compile!
9
9
 
10
10
  def compile!(verb, path, *args, &block)
11
- compile_without_sk!(verb, path, *args, &block).tap do |_, _, _, wrapper|
11
+ compile_without_sk!(verb, path, *args, &block).tap do |_, _, keys_or_wrapper, wrapper|
12
+ wrapper ||= keys_or_wrapper
13
+
12
14
  # Deal with the situation where the path is a regex, and the default behavior
13
15
  # of Ruby stringification produces an unreadable mess
14
16
  if path.is_a?(Regexp)
@@ -26,13 +26,11 @@ module Skylight
26
26
  if activate?
27
27
  if config
28
28
  begin
29
- config.validate!
30
-
31
29
  if Instrumenter.start!(config)
32
30
  app.middleware.insert 0, Middleware, config: config
33
31
  Rails.logger.info "[SKYLIGHT] [#{Skylight::VERSION}] Skylight agent enabled"
34
32
  else
35
- Rails.logger.info "[SKYLIGHT] [#{Skylight::VERSION}] Unable to start"
33
+ Rails.logger.info "[SKYLIGHT] [#{Skylight::VERSION}] Unable to start, see the Skylight logs for more details"
36
34
  end
37
35
  rescue ConfigError => e
38
36
  Rails.logger.error "[SKYLIGHT] [#{Skylight::VERSION}] #{e.message}; disabling Skylight agent"
@@ -88,7 +86,7 @@ module Skylight
88
86
  # Configure the log file destination
89
87
  if log_file = app.config.skylight.log_file
90
88
  config['log_file'] = log_file
91
- elsif !config.key?('log_file')
89
+ elsif !config.key?('log_file') && !config.on_heroku?
92
90
  config['log_file'] = File.join(Rails.root, 'log/skylight.log')
93
91
  end
94
92
 
@@ -86,8 +86,11 @@ module Skylight
86
86
 
87
87
  while curr = trace.notifications.pop
88
88
  if curr.name == name
89
- normalize_after(trace, curr.span, name, payload)
90
- trace.done(curr.span) if curr.span
89
+ begin
90
+ normalize_after(trace, curr.span, name, payload)
91
+ ensure
92
+ trace.done(curr.span) if curr.span
93
+ end
91
94
  return
92
95
  end
93
96
  end
@@ -57,6 +57,7 @@ module Skylight
57
57
  end
58
58
 
59
59
  def error(msg, *args)
60
+ raise sprintf(msg, *args) if ENV['SKYLIGHT_RAISE_ON_ERROR']
60
61
  log :error, msg, *args
61
62
  end
62
63
 
@@ -69,8 +70,13 @@ module Skylight
69
70
  alias fmt sprintf
70
71
 
71
72
  def log(level, msg, *args)
72
- return unless respond_to?(:config)
73
- return unless c = config
73
+ c = if respond_to?(:config)
74
+ config
75
+ elsif self.is_a?(Config)
76
+ self
77
+ end
78
+
79
+ return unless c
74
80
 
75
81
  if logger = c.logger
76
82
  return unless logger.respond_to?(level)
@@ -4,6 +4,7 @@ require 'net/http'
4
4
  require 'fileutils'
5
5
  require 'digest/sha2'
6
6
  require 'skylight/util/ssl'
7
+ require 'skylight/util/proxy'
7
8
 
8
9
  # Used from extconf.rb
9
10
  module Skylight
@@ -92,9 +93,9 @@ module Skylight
92
93
  when :success
93
94
  log "successfully downloaded native ext; out=#{out}"
94
95
  return extra
95
- else
96
+ when :redirect
96
97
  log "fetching native ext; uri=#{uri}; redirected=#{res}"
97
- uri = res
98
+ uri = extra
98
99
 
99
100
  next
100
101
  end
@@ -118,7 +119,7 @@ module Skylight
118
119
  end
119
120
 
120
121
  def http_get(host, port, use_ssl, path, out)
121
- if http_proxy = ENV['HTTP_PROXY'] || ENV['http_proxy']
122
+ if http_proxy = Proxy.detect_url(ENV)
122
123
  log "connecting with proxy: #{http_proxy}"
123
124
  uri = URI.parse(http_proxy)
124
125
  p_host, p_port = uri.host, uri.port
@@ -0,0 +1,12 @@
1
+ module Skylight
2
+ module Util
3
+ module Proxy
4
+ def self.detect_url(env)
5
+ if u = env['HTTP_PROXY'] || env['http_proxy']
6
+ u = "http://#{u}" unless u =~ %r[://]
7
+ u
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,4 +1,4 @@
1
1
  module Skylight
2
- VERSION = '1.0.0-beta4'
2
+ VERSION = '1.0.0-beta5'
3
3
  end
4
4
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skylight
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta4
4
+ version: 1.0.0.beta5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tilde, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-22 00:00:00.000000000 Z
11
+ date: 2016-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -47,6 +47,8 @@ files:
47
47
  - lib/skylight.rb
48
48
  - lib/skylight/api.rb
49
49
  - lib/skylight/cli.rb
50
+ - lib/skylight/cli/doctor.rb
51
+ - lib/skylight/cli/helpers.rb
50
52
  - lib/skylight/compat.rb
51
53
  - lib/skylight/config.rb
52
54
  - lib/skylight/core.rb
@@ -114,6 +116,7 @@ files:
114
116
  - lib/skylight/util/multi_io.rb
115
117
  - lib/skylight/util/native_ext_fetcher.rb
116
118
  - lib/skylight/util/platform.rb
119
+ - lib/skylight/util/proxy.rb
117
120
  - lib/skylight/util/ssl.rb
118
121
  - lib/skylight/vendor/active_support/notifications.rb
119
122
  - lib/skylight/vendor/active_support/notifications/fanout.rb
@@ -163,10 +166,6 @@ files:
163
166
  - lib/skylight/vendor/thread_safe/synchronized_cache_backend.rb
164
167
  - lib/skylight/version.rb
165
168
  - lib/skylight/vm/gc.rb
166
- - lib/sql_lexer.rb
167
- - lib/sql_lexer/lexer.rb
168
- - lib/sql_lexer/string_scanner.rb
169
- - lib/sql_lexer/version.rb
170
169
  homepage: http://www.skylight.io
171
170
  licenses: []
172
171
  metadata: {}
@@ -186,7 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
186
185
  version: 1.3.1
187
186
  requirements: []
188
187
  rubyforge_project:
189
- rubygems_version: 2.5.2
188
+ rubygems_version: 2.5.1
190
189
  signing_key:
191
190
  specification_version: 4
192
191
  summary: Skylight is a smart profiler for Rails apps
@@ -1,6 +0,0 @@
1
- require "sql_lexer/version"
2
- require "sql_lexer/string_scanner"
3
- require "sql_lexer/lexer"
4
-
5
- module SqlLexer
6
- end
@@ -1,579 +0,0 @@
1
- # encoding: utf-8
2
-
3
- require "strscan"
4
-
5
- module SqlLexer
6
- class Lexer
7
- # SQL identifiers and key words must begin with a letter (a-z, but also
8
- # letters with diacritical marks and non-Latin letters) or an underscore
9
- # (_). Subsequent characters in an identifier or key word can be letters,
10
- # underscores, digits (0-9), or dollar signs ($). Note that dollar signs
11
- # are not allowed in identifiers according to the letter of the SQL
12
- # standard, so their use might render applications less portable. The SQL
13
- # standard will not define a key word that contains digits or starts or
14
- # ends with an underscore, so identifiers of this form are safe against
15
- # possible conflict with future extensions of the standard.
16
- StartID = %q<\p{Alpha}_>
17
- PartID = %q<\p{Alnum}_$>
18
- OpPart = %q<\+|\-(?!-)|\*|/(?!\*)|\<|\>|=|~|!|@|#|%|\^|&|\||\?|\.|,|\(|\)>
19
- WS = %q< \t\r\n>
20
- OptWS = %Q<[#{WS}]*>
21
- End = %Q<;|$>
22
-
23
- InOp = %Q<IN(?=#{OptWS}\\()>
24
- ArrayOp = %q<ARRAY(?=\[)>
25
- ColonColonOp = %Q<::(?=[#{StartID}])>
26
- ArrayIndexOp = %q<\\[(?:\-?\d+(?::\-?\d+)?|NULL)\\]>
27
- SpecialOps = %Q<#{InOp}(?=[#{WS}])|#{ColonColonOp}|#{ArrayOp}|#{ArrayIndexOp}>
28
-
29
- StartQuotedID = %Q<">
30
- StartTickedID = %Q<`>
31
- StartString = %Q<[a-zA-Z]?'>
32
- StartDigit = %q<[\p{Digit}\.]>
33
-
34
- StartSelect = %Q<SELECT(?=(?:[#{WS}]|#{OpPart}))>
35
-
36
- # Binds that are also IDs do not need to be included here, since AfterOp (which uses StartBind)
37
- # also checks for StartAnyId
38
- StartBind = %Q<#{StartString}|#{StartDigit}|#{SpecialOps}>
39
-
40
- StartNonBind = %Q<#{StartQuotedID}|#{StartTickedID}|\\$(?=\\p{Digit})>
41
- TableNext = %Q<(#{OptWS}((?=#{StartQuotedID})|(?=#{StartTickedID}))|[#{WS}]+(?=[#{StartID}]))>
42
- StartAnyId = %Q<"#{StartID}>
43
- Placeholder = %q<\$\p{Digit}+>
44
-
45
- AfterID = %Q<[#{WS};#{StartNonBind}]|(?:#{OpPart})|(?:#{ColonColonOp})|(?:#{ArrayIndexOp})|$>
46
- ID = %Q<[#{StartID}][#{PartID}]*(?=#{AfterID})>
47
- AfterOp = %Q<[#{WS}]|[#{StartAnyId}]|[#{StartBind}]|(#{StartNonBind})|;|$>
48
- Op = %Q<(?:#{OpPart})+(?=#{AfterOp})>
49
- QuotedID = %Q<#{StartQuotedID}(?:[^"]|"")*">
50
- TickedID = %Q<#{StartTickedID}(?:[^`]|``)*`>
51
- NonBind = %Q<#{ID}|#{Op}|#{QuotedID}|#{TickedID}|#{Placeholder}>
52
- Type = %Q<[#{StartID}][#{PartID}]*(?:\\(\d+\\)|\\[\\])?(?=#{AfterID})>
53
- QuotedTable = %Q<#{TickedID}|#{QuotedID}>
54
-
55
- StringBody = %q{(?:''|(?<!\x5C)(?:\x5C\x5C)*\x5C'|[^'])*}
56
- String = %Q<#{StartString}#{StringBody}'>
57
-
58
- Digits = %q<\p{Digit}+>
59
- OptDigits = %q<\p{Digit}*>
60
- Exponent = %Q<e[+\-]?#{Digits}>
61
- OptExponent = %Q<(?:#{Exponent})?>
62
- HeadDecimal = %Q<#{Digits}\\.#{OptDigits}#{OptExponent}>
63
- TailDecimal = %Q<#{OptDigits}\\.#{Digits}#{OptExponent}>
64
- ExpDecimal = %Q<#{Digits}#{Exponent}>
65
-
66
- Number = %Q<#{HeadDecimal}|#{TailDecimal}|#{ExpDecimal}|#{Digits}>
67
-
68
- Literals = %Q<(?:NULL|TRUE|FALSE)(?=(?:[#{WS}]|#{OpPart}|#{End}))>
69
-
70
- TkWS = %r<[#{WS}]+>u
71
- TkOptWS = %r<[#{WS}]*>u
72
- TkOp = %r<[#{OpPart}]>u
73
- TkComment = %r<^#{OptWS}--.*$>u
74
- TkBlockCommentStart = %r</\*>u
75
- TkBlockCommentEnd = %r<\*/>u
76
- TkPlaceholder = %r<#{Placeholder}>u
77
- TkNonBind = %r<#{NonBind}>u
78
- TkType = %r<#{Type}>u
79
- TkQuotedTable = %r<#{QuotedTable}>iu
80
- TkUpdateTable = %r<UPDATE#{TableNext}>iu
81
- TkInsertTable = %r<INSERT[#{WS}]+INTO#{TableNext}>iu
82
- TkDeleteTable = %r<DELETE[#{WS}]+FROM#{TableNext}>iu
83
- TkFromTable = %r<FROM#{TableNext}>iu
84
- TkID = %r<#{ID}>u
85
- TkEnd = %r<;?[#{WS}]*>u
86
- TkBind = %r<#{String}|#{Number}|#{Literals}>u
87
- TkIn = %r<#{InOp}>iu
88
- TkColonColon = %r<#{ColonColonOp}>u
89
- TkArray = %r<#{ArrayOp}>iu
90
- TkArrayIndex = %r<#{ArrayIndexOp}>iu
91
- TkSpecialOp = %r<#{SpecialOps}>iu
92
- TkStartSelect = %r<#{StartSelect}>iu
93
- TkStartSubquery = %r<\(#{OptWS}#{StartSelect}>iu
94
- TkCloseParen = %r<#{OptWS}\)>u
95
-
96
- STATE_HANDLERS = {
97
- begin: :process_begin,
98
- first_token: :process_first_token,
99
- tokens: :process_tokens,
100
- bind: :process_bind,
101
- non_bind: :process_non_bind,
102
- placeholder: :process_placeholder,
103
- table_name: :process_table_name,
104
- end: :process_end,
105
- special: :process_special,
106
- subquery: :process_subquery,
107
- in: :process_in,
108
- array: :process_array
109
- }
110
-
111
- def self.bindify(string, binds=nil, strip_comments=false)
112
- scanner = instance(string)
113
- scanner.process(binds, strip_comments)
114
- [scanner.title, scanner.output, scanner.binds]
115
- end
116
-
117
- attr_reader :output, :binds, :title
118
-
119
- def self.pooled_value(name, default)
120
- key = :"__skylight_sql_#{name}"
121
-
122
- singleton_class.class_eval do
123
- define_method(name) do
124
- value = Thread.current[key] ||= default.dup
125
- value.clear
126
- value
127
- end
128
- end
129
-
130
- __send__(name)
131
- end
132
-
133
- SCANNER_KEY = :__skylight_sql_scanner
134
- LEXER_KEY = :__skylight_sql_lexer
135
-
136
- def self.scanner(string='')
137
- scanner = Thread.current[SCANNER_KEY] ||= StringScanner.new('')
138
- scanner.string = string
139
- scanner
140
- end
141
-
142
- def self.instance(string)
143
- lexer = Thread.current[LEXER_KEY] ||= new
144
- lexer.init(string)
145
- lexer
146
- end
147
-
148
- pooled_value :binds, []
149
- pooled_value :table, "*" * 20
150
-
151
- SPACE = " ".freeze
152
-
153
- DEBUG = ENV["SKYLIGHT_SQL_DEBUG"]
154
-
155
- def init(string)
156
- @state = :begin
157
- @debug = DEBUG
158
- @binds = self.class.binds
159
- @table = self.class.table
160
- @title = nil
161
- @bind = 0
162
-
163
- self.string = string
164
- end
165
-
166
- def string=(value)
167
- @input = value
168
-
169
- @scanner = self.class.scanner(value)
170
-
171
- # intentionally allocates; we need to return a new
172
- # string as part of this API
173
- @output = value.dup
174
- end
175
-
176
- PLACEHOLDER = "?".freeze
177
- UNKNOWN = "<unknown>".freeze
178
-
179
- def process(binds, strip_comments)
180
- process_comments(strip_comments)
181
-
182
- @operation = nil
183
- @provided_binds = binds
184
-
185
- while @state
186
- if @debug
187
- p @state
188
- p @scanner
189
- end
190
-
191
- __send__ STATE_HANDLERS[@state]
192
- end
193
-
194
- pos = 0
195
- removed = 0
196
-
197
- # intentionally allocates; the returned binds must
198
- # be in a newly produced array
199
- extracted_binds = Array.new(@binds.size / 2)
200
-
201
- if @operation && !@table.empty?
202
- @title = "" << @operation << SPACE << @table
203
- end
204
-
205
- while pos < @binds.size
206
- if @binds[pos] == nil
207
- extracted_binds[pos/2] = @binds[pos+1]
208
- else
209
- slice = @output[@binds[pos] - removed, @binds[pos+1]]
210
- @output[@binds[pos] - removed, @binds[pos+1]] = PLACEHOLDER
211
-
212
- extracted_binds[pos/2] = slice
213
- removed += (@binds[pos+1] - 1)
214
- end
215
-
216
- pos += 2
217
- end
218
-
219
- @binds = extracted_binds
220
- nil
221
- end
222
-
223
- def replace_comment(pos, length, strip)
224
- if strip
225
- # Dup the string if necessary so we aren't destructive to the original value
226
- if @input == @original_input
227
- @input = @input.dup
228
- @scanner.string = @input
229
- end
230
-
231
- # Replace the comment with a space to ensure valid SQL
232
- # Updating the input also updates the scanner
233
- @input[pos, length] = SPACE
234
- @output[pos, length] = SPACE
235
-
236
- # Move back to start of removed string
237
- @scanner.pos = pos
238
- else
239
- # Dup the string if necessary so we aren't destructive to the original value
240
- @scanner.string = @input.dup if @scanner.string == @original_input
241
-
242
- # Replace the comment with spaces
243
- @scanner.string[pos, length] = SPACE*length
244
- end
245
- end
246
-
247
- def process_comments(strip_comments)
248
- @original_input = @input
249
-
250
- # SQL treats comments as similar to whitespace
251
- # Here we replace all comments with spaces of the same length so as to not affect binds
252
-
253
- # Remove block comments
254
- # SQL allows for nested comments so this takes a bit more work
255
- while @scanner.skip_until(TkBlockCommentStart)
256
- count = 1
257
- pos = @scanner.charpos - 2
258
-
259
- while true
260
- # Determine whether we close the comment or start nesting
261
- next_open = @scanner.skip_until(TkBlockCommentStart)
262
- @scanner.unscan if next_open
263
- next_close = @scanner.skip_until(TkBlockCommentEnd)
264
- @scanner.unscan if next_close
265
-
266
- if next_open && next_open < next_close
267
- # We're nesting
268
- count += 1
269
- @scanner.skip_until(TkBlockCommentStart)
270
- else
271
- # We're closing
272
- count -= 1
273
- @scanner.skip_until(TkBlockCommentEnd)
274
- end
275
-
276
- if count > 10_000
277
- raise "The SQL '#{@scanner.string}' could not be parsed because of too many iterations in block comments"
278
- end
279
-
280
- if count == 0
281
- # We've closed all comments
282
- length = @scanner.charpos - pos
283
- replace_comment(pos, length, strip_comments)
284
- break
285
- end
286
- end
287
- end
288
-
289
- @scanner.reset
290
-
291
- # Remove single line comments
292
- while @scanner.skip_until(TkComment)
293
- pos = @scanner.charpos
294
- len = @scanner.matched_size
295
- replace_comment(pos-len, len, strip_comments)
296
- end
297
-
298
- @scanner.reset
299
- end
300
-
301
- def process_begin
302
- @scanner.skip(TkOptWS)
303
- @state = :first_token
304
- end
305
-
306
- OP_SELECT_FROM = "SELECT FROM".freeze
307
- OP_UPDATE = "UPDATE".freeze
308
- OP_INSERT_INTO = "INSERT INTO".freeze
309
- OP_DELETE_FROM = "DELETE FROM".freeze
310
-
311
- def process_first_token
312
- if @scanner.skip(TkStartSelect)
313
- @operation = OP_SELECT_FROM
314
- @state = :tokens
315
- else
316
- if @scanner.skip(TkUpdateTable)
317
- @operation = OP_UPDATE
318
- elsif @scanner.skip(TkInsertTable)
319
- @operation = OP_INSERT_INTO
320
- elsif @scanner.skip(TkDeleteTable)
321
- @operation = OP_DELETE_FROM
322
- end
323
-
324
- @state = :table_name
325
- end
326
- end
327
-
328
- def process_table_name
329
- pos = @scanner.pos
330
-
331
- if @scanner.skip(TkQuotedTable)
332
- copy_substr(@input, @table, pos + 1, @scanner.pos - 1)
333
- elsif @scanner.skip(TkID)
334
- copy_substr(@input, @table, pos, @scanner.pos)
335
- end
336
-
337
- @state = :tokens
338
- end
339
-
340
- def process_tokens
341
- @scanner.skip(TkOptWS)
342
-
343
- if @operation == OP_SELECT_FROM && @table.empty? && @scanner.skip(TkFromTable)
344
- @state = :table_name
345
- elsif @scanner.match?(TkSpecialOp)
346
- @state = :special
347
- elsif @scanner.match?(TkBind)
348
- @state = :bind
349
- elsif @scanner.match?(TkPlaceholder)
350
- @state = :placeholder
351
- elsif @scanner.match?(TkNonBind)
352
- @state = :non_bind
353
- else
354
- @state = :end
355
- end
356
- end
357
-
358
- def process_placeholder
359
- @scanner.skip(TkPlaceholder)
360
-
361
- binds << nil
362
-
363
- if !@provided_binds
364
- @binds << UNKNOWN
365
- elsif !@provided_binds[@bind]
366
- @binds << UNKNOWN
367
- else
368
- @binds << @provided_binds[@bind]
369
- end
370
-
371
- @bind += 1
372
-
373
- @state = :tokens
374
- end
375
-
376
- def process_special
377
- if @scanner.skip(TkIn)
378
- @scanner.skip(TkOptWS)
379
- if @scanner.skip(TkStartSubquery)
380
- @state = :subquery
381
- else
382
- @scanner.skip(/\(/u)
383
- @state = :in
384
- end
385
- elsif @scanner.skip(TkArray)
386
- @scanner.skip(/\[/u)
387
- @state = :array
388
- elsif @scanner.skip(TkColonColon)
389
- if @scanner.skip(TkType)
390
- @state = :tokens
391
- else
392
- @state = :end
393
- end
394
- elsif @scanner.skip(TkStartSubquery)
395
- @state = :subquery
396
- elsif @scanner.skip(TkArrayIndex)
397
- @state = :tokens
398
- end
399
- end
400
-
401
- def process_subquery
402
- nest = 1
403
- iterations = 0
404
-
405
- while nest > 0
406
- iterations += 1
407
-
408
- if iterations > 10_000
409
- raise "The SQL '#{@scanner.string}' could not be parsed because of too many iterations in subquery"
410
- end
411
-
412
- if @debug
413
- p @state
414
- p @scanner
415
- p nest
416
- p @scanner.peek(1)
417
- end
418
-
419
- if @scanner.skip(TkStartSubquery)
420
- nest += 1
421
- @state = :tokens
422
- elsif @scanner.skip(TkCloseParen)
423
- nest -= 1
424
- break if nest.zero?
425
- @state = :tokens
426
- elsif @state == :subquery
427
- @state = :tokens
428
- end
429
-
430
- __send__ STATE_HANDLERS[@state]
431
- end
432
-
433
- @state = :tokens
434
- end
435
-
436
- def process_in
437
- nest = 1
438
- iterations = 0
439
-
440
- @skip_binds = true
441
- pos = @scanner.charpos - 1
442
-
443
- while nest > 0
444
- iterations += 1
445
-
446
- if iterations > 10_000
447
- raise "The SQL '#{@scanner.string}' could not be parsed because of too many iterations in IN"
448
- end
449
-
450
- if @debug
451
- p @state
452
- p @scanner
453
- p nest
454
- end
455
-
456
- if @scanner.skip(/\(/u)
457
- nest += 1
458
- process_tokens
459
- elsif @scanner.skip(TkCloseParen)
460
- nest -= 1
461
- break if nest.zero?
462
- process_tokens
463
- else
464
- process_tokens
465
- end
466
-
467
- __send__ STATE_HANDLERS[@state]
468
- end
469
-
470
- @binds << pos
471
- @binds << @scanner.charpos - pos
472
-
473
- @skip_binds = false
474
-
475
- @state = :tokens
476
- end
477
-
478
- def process_array
479
- nest = 1
480
- iterations = 0
481
-
482
- @skip_binds = true
483
- pos = @scanner.charpos - 6
484
-
485
- while nest > 0
486
- iterations += 1
487
-
488
- if iterations > 10_000
489
- raise "The SQL '#{@scanner.string}' could not be parsed because of too many iterations in ARRAY"
490
- end
491
-
492
- if @debug
493
- p "array loop"
494
- p @state
495
- p @scanner
496
- end
497
-
498
- if @scanner.skip(/\[/u)
499
- nest += 1
500
- elsif @scanner.skip(/\]/u)
501
- nest -= 1
502
-
503
- break if nest.zero?
504
-
505
- # End of final nested array
506
- next if @scanner.skip(/#{TkOptWS}(?=\])/u)
507
- end
508
-
509
- # A NULL array
510
- next if @scanner.skip(/NULL/iu)
511
-
512
- # Another nested array
513
- next if @scanner.skip(/#{TkOptWS},#{TkOptWS}(?=\[)/u)
514
-
515
- process_tokens
516
-
517
- __send__ STATE_HANDLERS[@state]
518
- end
519
-
520
- @binds << pos
521
- @binds << @scanner.charpos - pos
522
-
523
- @skip_binds = false
524
-
525
- @state = :tokens
526
- end
527
-
528
- def process_non_bind
529
- @scanner.skip(TkNonBind)
530
- @state = :tokens
531
- end
532
-
533
- def process_bind
534
- pos = nil
535
-
536
- unless @skip_binds
537
- pos = @scanner.charpos
538
- end
539
-
540
- @scanner.skip(TkBind)
541
-
542
- unless @skip_binds
543
- @binds << pos
544
- @binds << @scanner.charpos - pos
545
- end
546
-
547
- @state = :tokens
548
- end
549
-
550
- def process_end
551
- if @scanner.skip(TkEnd)
552
- if @scanner.eos?
553
- @state = nil
554
- else
555
- process_tokens
556
- end
557
- end
558
-
559
- # We didn't hit EOS and we couldn't process any tokens
560
- if @state == :end
561
- raise "The SQL '#{@scanner.string}' could not be parsed"
562
- end
563
- end
564
-
565
- private
566
- def copy_substr(source, target, start_pos, end_pos)
567
- pos = start_pos
568
-
569
- while pos < end_pos
570
- target.concat source.getbyte(pos)
571
- pos += 1
572
- end
573
- end
574
-
575
- scanner
576
- instance('')
577
-
578
- end
579
- end