truex-skylight 0.6.0
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 +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
|