skylight 1.0.0.beta4 → 1.0.0.beta5

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.
@@ -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