truex-skylight 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +277 -0
- data/CLA.md +9 -0
- data/CONTRIBUTING.md +1 -0
- data/LICENSE.md +79 -0
- data/README.md +4 -0
- data/bin/skylight +3 -0
- data/ext/extconf.rb +186 -0
- data/ext/libskylight.yml +6 -0
- data/ext/skylight_memprof.c +115 -0
- data/ext/skylight_native.c +416 -0
- data/ext/skylight_native.h +20 -0
- data/lib/skylight.rb +2 -0
- data/lib/skylight/api.rb +79 -0
- data/lib/skylight/cli.rb +146 -0
- data/lib/skylight/compat.rb +47 -0
- data/lib/skylight/config.rb +498 -0
- data/lib/skylight/core.rb +122 -0
- data/lib/skylight/data/cacert.pem +3894 -0
- data/lib/skylight/formatters/http.rb +17 -0
- data/lib/skylight/gc.rb +107 -0
- data/lib/skylight/helpers.rb +137 -0
- data/lib/skylight/instrumenter.rb +290 -0
- data/lib/skylight/middleware.rb +75 -0
- data/lib/skylight/native.rb +69 -0
- data/lib/skylight/normalizers.rb +133 -0
- data/lib/skylight/normalizers/action_controller/process_action.rb +35 -0
- data/lib/skylight/normalizers/action_controller/send_file.rb +76 -0
- data/lib/skylight/normalizers/action_view/render_collection.rb +18 -0
- data/lib/skylight/normalizers/action_view/render_partial.rb +18 -0
- data/lib/skylight/normalizers/action_view/render_template.rb +18 -0
- data/lib/skylight/normalizers/active_record/sql.rb +79 -0
- data/lib/skylight/normalizers/active_support/cache.rb +50 -0
- data/lib/skylight/normalizers/active_support/cache_clear.rb +16 -0
- data/lib/skylight/normalizers/active_support/cache_decrement.rb +16 -0
- data/lib/skylight/normalizers/active_support/cache_delete.rb +16 -0
- data/lib/skylight/normalizers/active_support/cache_exist.rb +16 -0
- data/lib/skylight/normalizers/active_support/cache_fetch_hit.rb +16 -0
- data/lib/skylight/normalizers/active_support/cache_generate.rb +16 -0
- data/lib/skylight/normalizers/active_support/cache_increment.rb +16 -0
- data/lib/skylight/normalizers/active_support/cache_read.rb +16 -0
- data/lib/skylight/normalizers/active_support/cache_read_multi.rb +16 -0
- data/lib/skylight/normalizers/active_support/cache_write.rb +16 -0
- data/lib/skylight/normalizers/default.rb +21 -0
- data/lib/skylight/normalizers/moped/query.rb +141 -0
- data/lib/skylight/probes.rb +91 -0
- data/lib/skylight/probes/excon.rb +25 -0
- data/lib/skylight/probes/excon/middleware.rb +65 -0
- data/lib/skylight/probes/net_http.rb +44 -0
- data/lib/skylight/probes/redis.rb +30 -0
- data/lib/skylight/probes/sequel.rb +30 -0
- data/lib/skylight/probes/sinatra.rb +74 -0
- data/lib/skylight/probes/tilt.rb +27 -0
- data/lib/skylight/railtie.rb +122 -0
- data/lib/skylight/sinatra.rb +4 -0
- data/lib/skylight/subscriber.rb +92 -0
- data/lib/skylight/trace.rb +191 -0
- data/lib/skylight/util.rb +16 -0
- data/lib/skylight/util/allocation_free.rb +17 -0
- data/lib/skylight/util/clock.rb +53 -0
- data/lib/skylight/util/gzip.rb +15 -0
- data/lib/skylight/util/hostname.rb +17 -0
- data/lib/skylight/util/http.rb +218 -0
- data/lib/skylight/util/inflector.rb +110 -0
- data/lib/skylight/util/logging.rb +87 -0
- data/lib/skylight/util/multi_io.rb +21 -0
- data/lib/skylight/util/native_ext_fetcher.rb +205 -0
- data/lib/skylight/util/platform.rb +67 -0
- data/lib/skylight/util/ssl.rb +50 -0
- data/lib/skylight/vendor/active_support/notifications.rb +207 -0
- data/lib/skylight/vendor/active_support/notifications/fanout.rb +159 -0
- data/lib/skylight/vendor/active_support/notifications/instrumenter.rb +72 -0
- data/lib/skylight/vendor/active_support/per_thread_registry.rb +52 -0
- data/lib/skylight/vendor/cli/highline.rb +1034 -0
- data/lib/skylight/vendor/cli/highline/color_scheme.rb +134 -0
- data/lib/skylight/vendor/cli/highline/compatibility.rb +16 -0
- data/lib/skylight/vendor/cli/highline/import.rb +41 -0
- data/lib/skylight/vendor/cli/highline/menu.rb +381 -0
- data/lib/skylight/vendor/cli/highline/question.rb +481 -0
- data/lib/skylight/vendor/cli/highline/simulate.rb +48 -0
- data/lib/skylight/vendor/cli/highline/string_extensions.rb +111 -0
- data/lib/skylight/vendor/cli/highline/style.rb +181 -0
- data/lib/skylight/vendor/cli/highline/system_extensions.rb +242 -0
- data/lib/skylight/vendor/cli/thor.rb +473 -0
- data/lib/skylight/vendor/cli/thor/actions.rb +318 -0
- data/lib/skylight/vendor/cli/thor/actions/create_file.rb +105 -0
- data/lib/skylight/vendor/cli/thor/actions/create_link.rb +60 -0
- data/lib/skylight/vendor/cli/thor/actions/directory.rb +119 -0
- data/lib/skylight/vendor/cli/thor/actions/empty_directory.rb +137 -0
- data/lib/skylight/vendor/cli/thor/actions/file_manipulation.rb +314 -0
- data/lib/skylight/vendor/cli/thor/actions/inject_into_file.rb +109 -0
- data/lib/skylight/vendor/cli/thor/base.rb +652 -0
- data/lib/skylight/vendor/cli/thor/command.rb +136 -0
- data/lib/skylight/vendor/cli/thor/core_ext/hash_with_indifferent_access.rb +80 -0
- data/lib/skylight/vendor/cli/thor/core_ext/io_binary_read.rb +12 -0
- data/lib/skylight/vendor/cli/thor/core_ext/ordered_hash.rb +100 -0
- data/lib/skylight/vendor/cli/thor/error.rb +28 -0
- data/lib/skylight/vendor/cli/thor/group.rb +282 -0
- data/lib/skylight/vendor/cli/thor/invocation.rb +172 -0
- data/lib/skylight/vendor/cli/thor/parser.rb +4 -0
- data/lib/skylight/vendor/cli/thor/parser/argument.rb +74 -0
- data/lib/skylight/vendor/cli/thor/parser/arguments.rb +171 -0
- data/lib/skylight/vendor/cli/thor/parser/option.rb +121 -0
- data/lib/skylight/vendor/cli/thor/parser/options.rb +218 -0
- data/lib/skylight/vendor/cli/thor/rake_compat.rb +72 -0
- data/lib/skylight/vendor/cli/thor/runner.rb +322 -0
- data/lib/skylight/vendor/cli/thor/shell.rb +88 -0
- data/lib/skylight/vendor/cli/thor/shell/basic.rb +393 -0
- data/lib/skylight/vendor/cli/thor/shell/color.rb +148 -0
- data/lib/skylight/vendor/cli/thor/shell/html.rb +127 -0
- data/lib/skylight/vendor/cli/thor/util.rb +270 -0
- data/lib/skylight/vendor/cli/thor/version.rb +3 -0
- data/lib/skylight/vendor/thread_safe.rb +126 -0
- data/lib/skylight/vendor/thread_safe/non_concurrent_cache_backend.rb +133 -0
- data/lib/skylight/vendor/thread_safe/synchronized_cache_backend.rb +76 -0
- data/lib/skylight/version.rb +4 -0
- data/lib/skylight/vm/gc.rb +70 -0
- data/lib/sql_lexer.rb +6 -0
- data/lib/sql_lexer/lexer.rb +579 -0
- data/lib/sql_lexer/string_scanner.rb +11 -0
- data/lib/sql_lexer/version.rb +3 -0
- metadata +179 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module ThreadSafe
|
4
|
+
autoload :NonConcurrentCacheBackend, 'skylight/vendor/thread_safe/non_concurrent_cache_backend'
|
5
|
+
autoload :SynchronizedCacheBackend, 'skylight/vendor/thread_safe/synchronized_cache_backend'
|
6
|
+
|
7
|
+
ConcurrentCacheBackend = SynchronizedCacheBackend
|
8
|
+
|
9
|
+
class Cache < ConcurrentCacheBackend
|
10
|
+
KEY_ERROR = defined?(KeyError) ? KeyError : IndexError # there is no KeyError in 1.8 mode
|
11
|
+
|
12
|
+
def initialize(options = nil, &block)
|
13
|
+
if options.kind_of?(::Hash)
|
14
|
+
validate_options_hash!(options)
|
15
|
+
else
|
16
|
+
options = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
super(options)
|
20
|
+
@default_proc = block
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](key)
|
24
|
+
if value = super
|
25
|
+
value
|
26
|
+
elsif @default_proc && !key?(key)
|
27
|
+
@default_proc.call(self, key)
|
28
|
+
else
|
29
|
+
value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch(key, default_value = NULL)
|
34
|
+
if NULL != (value = get_or_default(key, NULL))
|
35
|
+
value
|
36
|
+
elsif block_given?
|
37
|
+
yield key
|
38
|
+
elsif NULL != default_value
|
39
|
+
default_value
|
40
|
+
else
|
41
|
+
raise KEY_ERROR, 'key not found'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def put_if_absent(key, value)
|
46
|
+
computed = false
|
47
|
+
result = compute_if_absent(key) do
|
48
|
+
computed = true
|
49
|
+
value
|
50
|
+
end
|
51
|
+
computed ? nil : result
|
52
|
+
end unless method_defined?(:put_if_absent)
|
53
|
+
|
54
|
+
def value?(value)
|
55
|
+
each_value do |v|
|
56
|
+
return true if value.equal?(v)
|
57
|
+
end
|
58
|
+
false
|
59
|
+
end unless method_defined?(:value?)
|
60
|
+
|
61
|
+
def keys
|
62
|
+
arr = []
|
63
|
+
each_pair {|k, v| arr << k}
|
64
|
+
arr
|
65
|
+
end unless method_defined?(:keys)
|
66
|
+
|
67
|
+
def values
|
68
|
+
arr = []
|
69
|
+
each_pair {|k, v| arr << v}
|
70
|
+
arr
|
71
|
+
end unless method_defined?(:values)
|
72
|
+
|
73
|
+
def each_key
|
74
|
+
each_pair {|k, v| yield k}
|
75
|
+
end unless method_defined?(:each_key)
|
76
|
+
|
77
|
+
def each_value
|
78
|
+
each_pair {|k, v| yield v}
|
79
|
+
end unless method_defined?(:each_value)
|
80
|
+
|
81
|
+
def empty?
|
82
|
+
each_pair {|k, v| return false}
|
83
|
+
true
|
84
|
+
end unless method_defined?(:empty?)
|
85
|
+
|
86
|
+
def size
|
87
|
+
count = 0
|
88
|
+
each_pair {|k, v| count += 1}
|
89
|
+
count
|
90
|
+
end unless method_defined?(:size)
|
91
|
+
|
92
|
+
def marshal_dump
|
93
|
+
raise TypeError, "can't dump hash with default proc" if @default_proc
|
94
|
+
h = {}
|
95
|
+
each_pair {|k, v| h[k] = v}
|
96
|
+
h
|
97
|
+
end
|
98
|
+
|
99
|
+
def marshal_load(hash)
|
100
|
+
initialize
|
101
|
+
populate_from(hash)
|
102
|
+
end
|
103
|
+
|
104
|
+
undef :freeze
|
105
|
+
|
106
|
+
private
|
107
|
+
def initialize_copy(other)
|
108
|
+
super
|
109
|
+
populate_from(other)
|
110
|
+
end
|
111
|
+
|
112
|
+
def populate_from(hash)
|
113
|
+
hash.each_pair {|k, v| self[k] = v}
|
114
|
+
self
|
115
|
+
end
|
116
|
+
|
117
|
+
def validate_options_hash!(options)
|
118
|
+
if (initial_capacity = options[:initial_capacity]) && (!initial_capacity.kind_of?(Fixnum) || initial_capacity < 0)
|
119
|
+
raise ArgumentError, ":initial_capacity must be a positive Fixnum"
|
120
|
+
end
|
121
|
+
if (load_factor = options[:load_factor]) && (!load_factor.kind_of?(Numeric) || load_factor <= 0 || load_factor > 1)
|
122
|
+
raise ArgumentError, ":load_factor must be a number between 0 and 1"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module ThreadSafe
|
2
|
+
class NonConcurrentCacheBackend
|
3
|
+
# WARNING: all public methods of the class must operate on the @backend directly without calling each other. This is important
|
4
|
+
# because of the SynchronizedCacheBackend which uses a non-reentrant mutex for perfomance reasons.
|
5
|
+
def initialize(options = nil)
|
6
|
+
@backend = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](key)
|
10
|
+
@backend[key]
|
11
|
+
end
|
12
|
+
|
13
|
+
def []=(key, value)
|
14
|
+
@backend[key] = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def compute_if_absent(key)
|
18
|
+
if NULL != (stored_value = @backend.fetch(key, NULL))
|
19
|
+
stored_value
|
20
|
+
else
|
21
|
+
@backend[key] = yield
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def replace_pair(key, old_value, new_value)
|
26
|
+
if pair?(key, old_value)
|
27
|
+
@backend[key] = new_value
|
28
|
+
true
|
29
|
+
else
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def replace_if_exists(key, new_value)
|
35
|
+
if NULL != (stored_value = @backend.fetch(key, NULL))
|
36
|
+
@backend[key] = new_value
|
37
|
+
stored_value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def compute_if_present(key)
|
42
|
+
if NULL != (stored_value = @backend.fetch(key, NULL))
|
43
|
+
store_computed_value(key, yield(stored_value))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def compute(key)
|
48
|
+
store_computed_value(key, yield(@backend[key]))
|
49
|
+
end
|
50
|
+
|
51
|
+
def merge_pair(key, value)
|
52
|
+
if NULL == (stored_value = @backend.fetch(key, NULL))
|
53
|
+
@backend[key] = value
|
54
|
+
else
|
55
|
+
store_computed_value(key, yield(stored_value))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def get_and_set(key, value)
|
60
|
+
stored_value = @backend[key]
|
61
|
+
@backend[key] = value
|
62
|
+
stored_value
|
63
|
+
end
|
64
|
+
|
65
|
+
def key?(key)
|
66
|
+
@backend.key?(key)
|
67
|
+
end
|
68
|
+
|
69
|
+
def value?(value)
|
70
|
+
@backend.value?(value)
|
71
|
+
end
|
72
|
+
|
73
|
+
def delete(key)
|
74
|
+
@backend.delete(key)
|
75
|
+
end
|
76
|
+
|
77
|
+
def delete_pair(key, value)
|
78
|
+
if pair?(key, value)
|
79
|
+
@backend.delete(key)
|
80
|
+
true
|
81
|
+
else
|
82
|
+
false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def clear
|
87
|
+
@backend.clear
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
def each_pair
|
92
|
+
dupped_backend.each_pair do |k, v|
|
93
|
+
yield k, v
|
94
|
+
end
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
def size
|
99
|
+
@backend.size
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_or_default(key, default_value)
|
103
|
+
@backend.fetch(key, default_value)
|
104
|
+
end
|
105
|
+
|
106
|
+
alias_method :_get, :[]
|
107
|
+
alias_method :_set, :[]=
|
108
|
+
private :_get, :_set
|
109
|
+
private
|
110
|
+
def initialize_copy(other)
|
111
|
+
super
|
112
|
+
@backend = {}
|
113
|
+
self
|
114
|
+
end
|
115
|
+
|
116
|
+
def dupped_backend
|
117
|
+
@backend.dup
|
118
|
+
end
|
119
|
+
|
120
|
+
def pair?(key, expected_value)
|
121
|
+
NULL != (stored_value = @backend.fetch(key, NULL)) && expected_value.equal?(stored_value)
|
122
|
+
end
|
123
|
+
|
124
|
+
def store_computed_value(key, new_value)
|
125
|
+
if new_value.nil?
|
126
|
+
@backend.delete(key)
|
127
|
+
nil
|
128
|
+
else
|
129
|
+
@backend[key] = new_value
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module ThreadSafe
|
2
|
+
class SynchronizedCacheBackend < NonConcurrentCacheBackend
|
3
|
+
require 'mutex_m'
|
4
|
+
include Mutex_m
|
5
|
+
# WARNING: Mutex_m is a non-reentrant lock, so the synchronized methods are not allowed to call each other.
|
6
|
+
|
7
|
+
def [](key)
|
8
|
+
synchronize { super }
|
9
|
+
end
|
10
|
+
|
11
|
+
def []=(key, value)
|
12
|
+
synchronize { super }
|
13
|
+
end
|
14
|
+
|
15
|
+
def compute_if_absent(key)
|
16
|
+
synchronize { super }
|
17
|
+
end
|
18
|
+
|
19
|
+
def compute_if_present(key)
|
20
|
+
synchronize { super }
|
21
|
+
end
|
22
|
+
|
23
|
+
def compute(key)
|
24
|
+
synchronize { super }
|
25
|
+
end
|
26
|
+
|
27
|
+
def merge_pair(key, value)
|
28
|
+
synchronize { super }
|
29
|
+
end
|
30
|
+
|
31
|
+
def replace_pair(key, old_value, new_value)
|
32
|
+
synchronize { super }
|
33
|
+
end
|
34
|
+
|
35
|
+
def replace_if_exists(key, new_value)
|
36
|
+
synchronize { super }
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_and_set(key, value)
|
40
|
+
synchronize { super }
|
41
|
+
end
|
42
|
+
|
43
|
+
def key?(key)
|
44
|
+
synchronize { super }
|
45
|
+
end
|
46
|
+
|
47
|
+
def value?(value)
|
48
|
+
synchronize { super }
|
49
|
+
end
|
50
|
+
|
51
|
+
def delete(key)
|
52
|
+
synchronize { super }
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete_pair(key, value)
|
56
|
+
synchronize { super }
|
57
|
+
end
|
58
|
+
|
59
|
+
def clear
|
60
|
+
synchronize { super }
|
61
|
+
end
|
62
|
+
|
63
|
+
def size
|
64
|
+
synchronize { super }
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_or_default(key, default_value)
|
68
|
+
synchronize { super }
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def dupped_backend
|
73
|
+
synchronize { super }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
|
2
|
+
module Skylight
|
3
|
+
# @api private
|
4
|
+
module VM
|
5
|
+
if defined?(JRUBY_VERSION)
|
6
|
+
|
7
|
+
# This doesn't quite work as we would like it. I believe that the GC
|
8
|
+
# statistics includes time that is not stop-the-world, this does not
|
9
|
+
# necessarily take time away from the application.
|
10
|
+
#
|
11
|
+
# require 'java'
|
12
|
+
# class GC
|
13
|
+
# def initialize
|
14
|
+
# @factory = Java::JavaLangManagement::ManagementFactory
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# def enable
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# def total_time
|
21
|
+
# res = 0.0
|
22
|
+
#
|
23
|
+
# @factory.garbage_collector_mx_beans.each do |mx|
|
24
|
+
# res += (mx.collection_time.to_f / 1_000.0)
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# res
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
|
31
|
+
elsif defined?(::GC::Profiler)
|
32
|
+
|
33
|
+
class GC
|
34
|
+
def initialize
|
35
|
+
@total = 0
|
36
|
+
end
|
37
|
+
|
38
|
+
def enable
|
39
|
+
::GC::Profiler.enable
|
40
|
+
end
|
41
|
+
|
42
|
+
def total_time
|
43
|
+
# Reported in seconds
|
44
|
+
run = (::GC::Profiler.total_time * 1_000_000).to_i
|
45
|
+
|
46
|
+
if run > 0
|
47
|
+
::GC::Profiler.clear
|
48
|
+
end
|
49
|
+
|
50
|
+
@total += run
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
# Fallback
|
57
|
+
unless defined?(VM::GC)
|
58
|
+
|
59
|
+
class GC
|
60
|
+
def enable
|
61
|
+
end
|
62
|
+
|
63
|
+
def total_time
|
64
|
+
0
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/sql_lexer.rb
ADDED
@@ -0,0 +1,579 @@
|
|
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["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
|