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