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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -20
- data/bin/skylight +1 -1
- data/lib/skylight/api.rb +69 -29
- data/lib/skylight/cli.rb +78 -109
- data/lib/skylight/cli/doctor.rb +132 -0
- data/lib/skylight/cli/helpers.rb +32 -0
- data/lib/skylight/config.rb +131 -68
- data/lib/skylight/core.rb +3 -1
- data/lib/skylight/instrumenter.rb +5 -32
- data/lib/skylight/normalizers/action_controller/process_action.rb +12 -4
- data/lib/skylight/normalizers/active_record/sql.rb +0 -17
- data/lib/skylight/normalizers/grape/endpoint.rb +1 -1
- data/lib/skylight/probes/action_controller.rb +9 -2
- data/lib/skylight/probes/sequel.rb +8 -5
- data/lib/skylight/probes/sinatra.rb +3 -1
- data/lib/skylight/railtie.rb +2 -4
- data/lib/skylight/subscriber.rb +5 -2
- data/lib/skylight/util/logging.rb +8 -2
- data/lib/skylight/util/native_ext_fetcher.rb +4 -3
- data/lib/skylight/util/proxy.rb +12 -0
- data/lib/skylight/version.rb +1 -1
- metadata +6 -7
- data/lib/sql_lexer.rb +0 -6
- data/lib/sql_lexer/lexer.rb +0 -579
- data/lib/sql_lexer/string_scanner.rb +0 -11
- data/lib/sql_lexer/version.rb +0 -3
@@ -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
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
12
|
-
|
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
|
-
|
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 |_, _,
|
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)
|
data/lib/skylight/railtie.rb
CHANGED
@@ -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
|
|
data/lib/skylight/subscriber.rb
CHANGED
@@ -86,8 +86,11 @@ module Skylight
|
|
86
86
|
|
87
87
|
while curr = trace.notifications.pop
|
88
88
|
if curr.name == name
|
89
|
-
|
90
|
-
|
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
|
-
|
73
|
-
|
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
|
-
|
96
|
+
when :redirect
|
96
97
|
log "fetching native ext; uri=#{uri}; redirected=#{res}"
|
97
|
-
uri =
|
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
|
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
|
data/lib/skylight/version.rb
CHANGED
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.
|
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-
|
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.
|
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
|
data/lib/sql_lexer.rb
DELETED
data/lib/sql_lexer/lexer.rb
DELETED
@@ -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
|