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