truex-skylight 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +277 -0
  3. data/CLA.md +9 -0
  4. data/CONTRIBUTING.md +1 -0
  5. data/LICENSE.md +79 -0
  6. data/README.md +4 -0
  7. data/bin/skylight +3 -0
  8. data/ext/extconf.rb +186 -0
  9. data/ext/libskylight.yml +6 -0
  10. data/ext/skylight_memprof.c +115 -0
  11. data/ext/skylight_native.c +416 -0
  12. data/ext/skylight_native.h +20 -0
  13. data/lib/skylight.rb +2 -0
  14. data/lib/skylight/api.rb +79 -0
  15. data/lib/skylight/cli.rb +146 -0
  16. data/lib/skylight/compat.rb +47 -0
  17. data/lib/skylight/config.rb +498 -0
  18. data/lib/skylight/core.rb +122 -0
  19. data/lib/skylight/data/cacert.pem +3894 -0
  20. data/lib/skylight/formatters/http.rb +17 -0
  21. data/lib/skylight/gc.rb +107 -0
  22. data/lib/skylight/helpers.rb +137 -0
  23. data/lib/skylight/instrumenter.rb +290 -0
  24. data/lib/skylight/middleware.rb +75 -0
  25. data/lib/skylight/native.rb +69 -0
  26. data/lib/skylight/normalizers.rb +133 -0
  27. data/lib/skylight/normalizers/action_controller/process_action.rb +35 -0
  28. data/lib/skylight/normalizers/action_controller/send_file.rb +76 -0
  29. data/lib/skylight/normalizers/action_view/render_collection.rb +18 -0
  30. data/lib/skylight/normalizers/action_view/render_partial.rb +18 -0
  31. data/lib/skylight/normalizers/action_view/render_template.rb +18 -0
  32. data/lib/skylight/normalizers/active_record/sql.rb +79 -0
  33. data/lib/skylight/normalizers/active_support/cache.rb +50 -0
  34. data/lib/skylight/normalizers/active_support/cache_clear.rb +16 -0
  35. data/lib/skylight/normalizers/active_support/cache_decrement.rb +16 -0
  36. data/lib/skylight/normalizers/active_support/cache_delete.rb +16 -0
  37. data/lib/skylight/normalizers/active_support/cache_exist.rb +16 -0
  38. data/lib/skylight/normalizers/active_support/cache_fetch_hit.rb +16 -0
  39. data/lib/skylight/normalizers/active_support/cache_generate.rb +16 -0
  40. data/lib/skylight/normalizers/active_support/cache_increment.rb +16 -0
  41. data/lib/skylight/normalizers/active_support/cache_read.rb +16 -0
  42. data/lib/skylight/normalizers/active_support/cache_read_multi.rb +16 -0
  43. data/lib/skylight/normalizers/active_support/cache_write.rb +16 -0
  44. data/lib/skylight/normalizers/default.rb +21 -0
  45. data/lib/skylight/normalizers/moped/query.rb +141 -0
  46. data/lib/skylight/probes.rb +91 -0
  47. data/lib/skylight/probes/excon.rb +25 -0
  48. data/lib/skylight/probes/excon/middleware.rb +65 -0
  49. data/lib/skylight/probes/net_http.rb +44 -0
  50. data/lib/skylight/probes/redis.rb +30 -0
  51. data/lib/skylight/probes/sequel.rb +30 -0
  52. data/lib/skylight/probes/sinatra.rb +74 -0
  53. data/lib/skylight/probes/tilt.rb +27 -0
  54. data/lib/skylight/railtie.rb +122 -0
  55. data/lib/skylight/sinatra.rb +4 -0
  56. data/lib/skylight/subscriber.rb +92 -0
  57. data/lib/skylight/trace.rb +191 -0
  58. data/lib/skylight/util.rb +16 -0
  59. data/lib/skylight/util/allocation_free.rb +17 -0
  60. data/lib/skylight/util/clock.rb +53 -0
  61. data/lib/skylight/util/gzip.rb +15 -0
  62. data/lib/skylight/util/hostname.rb +17 -0
  63. data/lib/skylight/util/http.rb +218 -0
  64. data/lib/skylight/util/inflector.rb +110 -0
  65. data/lib/skylight/util/logging.rb +87 -0
  66. data/lib/skylight/util/multi_io.rb +21 -0
  67. data/lib/skylight/util/native_ext_fetcher.rb +205 -0
  68. data/lib/skylight/util/platform.rb +67 -0
  69. data/lib/skylight/util/ssl.rb +50 -0
  70. data/lib/skylight/vendor/active_support/notifications.rb +207 -0
  71. data/lib/skylight/vendor/active_support/notifications/fanout.rb +159 -0
  72. data/lib/skylight/vendor/active_support/notifications/instrumenter.rb +72 -0
  73. data/lib/skylight/vendor/active_support/per_thread_registry.rb +52 -0
  74. data/lib/skylight/vendor/cli/highline.rb +1034 -0
  75. data/lib/skylight/vendor/cli/highline/color_scheme.rb +134 -0
  76. data/lib/skylight/vendor/cli/highline/compatibility.rb +16 -0
  77. data/lib/skylight/vendor/cli/highline/import.rb +41 -0
  78. data/lib/skylight/vendor/cli/highline/menu.rb +381 -0
  79. data/lib/skylight/vendor/cli/highline/question.rb +481 -0
  80. data/lib/skylight/vendor/cli/highline/simulate.rb +48 -0
  81. data/lib/skylight/vendor/cli/highline/string_extensions.rb +111 -0
  82. data/lib/skylight/vendor/cli/highline/style.rb +181 -0
  83. data/lib/skylight/vendor/cli/highline/system_extensions.rb +242 -0
  84. data/lib/skylight/vendor/cli/thor.rb +473 -0
  85. data/lib/skylight/vendor/cli/thor/actions.rb +318 -0
  86. data/lib/skylight/vendor/cli/thor/actions/create_file.rb +105 -0
  87. data/lib/skylight/vendor/cli/thor/actions/create_link.rb +60 -0
  88. data/lib/skylight/vendor/cli/thor/actions/directory.rb +119 -0
  89. data/lib/skylight/vendor/cli/thor/actions/empty_directory.rb +137 -0
  90. data/lib/skylight/vendor/cli/thor/actions/file_manipulation.rb +314 -0
  91. data/lib/skylight/vendor/cli/thor/actions/inject_into_file.rb +109 -0
  92. data/lib/skylight/vendor/cli/thor/base.rb +652 -0
  93. data/lib/skylight/vendor/cli/thor/command.rb +136 -0
  94. data/lib/skylight/vendor/cli/thor/core_ext/hash_with_indifferent_access.rb +80 -0
  95. data/lib/skylight/vendor/cli/thor/core_ext/io_binary_read.rb +12 -0
  96. data/lib/skylight/vendor/cli/thor/core_ext/ordered_hash.rb +100 -0
  97. data/lib/skylight/vendor/cli/thor/error.rb +28 -0
  98. data/lib/skylight/vendor/cli/thor/group.rb +282 -0
  99. data/lib/skylight/vendor/cli/thor/invocation.rb +172 -0
  100. data/lib/skylight/vendor/cli/thor/parser.rb +4 -0
  101. data/lib/skylight/vendor/cli/thor/parser/argument.rb +74 -0
  102. data/lib/skylight/vendor/cli/thor/parser/arguments.rb +171 -0
  103. data/lib/skylight/vendor/cli/thor/parser/option.rb +121 -0
  104. data/lib/skylight/vendor/cli/thor/parser/options.rb +218 -0
  105. data/lib/skylight/vendor/cli/thor/rake_compat.rb +72 -0
  106. data/lib/skylight/vendor/cli/thor/runner.rb +322 -0
  107. data/lib/skylight/vendor/cli/thor/shell.rb +88 -0
  108. data/lib/skylight/vendor/cli/thor/shell/basic.rb +393 -0
  109. data/lib/skylight/vendor/cli/thor/shell/color.rb +148 -0
  110. data/lib/skylight/vendor/cli/thor/shell/html.rb +127 -0
  111. data/lib/skylight/vendor/cli/thor/util.rb +270 -0
  112. data/lib/skylight/vendor/cli/thor/version.rb +3 -0
  113. data/lib/skylight/vendor/thread_safe.rb +126 -0
  114. data/lib/skylight/vendor/thread_safe/non_concurrent_cache_backend.rb +133 -0
  115. data/lib/skylight/vendor/thread_safe/synchronized_cache_backend.rb +76 -0
  116. data/lib/skylight/version.rb +4 -0
  117. data/lib/skylight/vm/gc.rb +70 -0
  118. data/lib/sql_lexer.rb +6 -0
  119. data/lib/sql_lexer/lexer.rb +579 -0
  120. data/lib/sql_lexer/string_scanner.rb +11 -0
  121. data/lib/sql_lexer/version.rb +3 -0
  122. metadata +179 -0
@@ -0,0 +1,3 @@
1
+ class Thor
2
+ VERSION = "0.18.1"
3
+ end
@@ -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,4 @@
1
+ module Skylight
2
+ VERSION = '0.6.0'
3
+ end
4
+
@@ -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
@@ -0,0 +1,6 @@
1
+ require "sql_lexer/version"
2
+ require "sql_lexer/string_scanner"
3
+ require "sql_lexer/lexer"
4
+
5
+ module SqlLexer
6
+ end
@@ -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