pg_pipeline 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/lib/pg_pipeline/bounded_queue.rb +8 -6
- data/lib/pg_pipeline/client.rb +55 -37
- data/lib/pg_pipeline/connection_driver.rb +349 -272
- data/lib/pg_pipeline/pool.rb +229 -157
- data/lib/pg_pipeline/prepared_statement.rb +5 -5
- data/lib/pg_pipeline/request.rb +18 -17
- data/lib/pg_pipeline/runtime/notification.rb +2 -1
- data/lib/pg_pipeline/runtime/semaphore.rb +3 -3
- data/lib/pg_pipeline/runtime/task.rb +14 -8
- data/lib/pg_pipeline/runtime.rb +17 -18
- data/lib/pg_pipeline/server_caps.rb +3 -3
- data/lib/pg_pipeline/session.rb +9 -7
- data/lib/pg_pipeline/session_guard.rb +108 -73
- data/lib/pg_pipeline/transaction.rb +33 -34
- data/lib/pg_pipeline/type_maps.rb +37 -22
- data/lib/pg_pipeline/version.rb +1 -1
- metadata +1 -1
data/lib/pg_pipeline/request.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'errors'
|
|
4
4
|
|
|
5
5
|
module PgPipeline
|
|
6
6
|
class Request
|
|
@@ -119,7 +119,7 @@ module PgPipeline
|
|
|
119
119
|
|
|
120
120
|
def snapshot_params(params)
|
|
121
121
|
return EMPTY_PARAMS if params.nil?
|
|
122
|
-
raise ArgumentError,
|
|
122
|
+
raise ArgumentError, 'params must be an Array' unless params.is_a?(Array)
|
|
123
123
|
return EMPTY_PARAMS if params.empty?
|
|
124
124
|
|
|
125
125
|
return params if params.frozen? && immutable_values?(params)
|
|
@@ -160,24 +160,23 @@ module PgPipeline
|
|
|
160
160
|
|
|
161
161
|
def snapshot_name(name)
|
|
162
162
|
value = name.to_s
|
|
163
|
-
raise ArgumentError,
|
|
163
|
+
raise ArgumentError, 'statement_name must not be empty' if value.empty?
|
|
164
164
|
|
|
165
165
|
value.frozen? ? value : value.dup.freeze
|
|
166
166
|
end
|
|
167
167
|
|
|
168
168
|
def snapshot_param_types(param_types)
|
|
169
169
|
return nil if param_types.nil?
|
|
170
|
-
raise ArgumentError,
|
|
170
|
+
raise ArgumentError, 'param_types must be an Array or nil' unless param_types.is_a?(Array)
|
|
171
171
|
|
|
172
172
|
param_types.map { |oid| oid.nil? ? nil : Integer(oid) }.freeze
|
|
173
173
|
rescue ArgumentError, TypeError
|
|
174
|
-
raise ArgumentError,
|
|
174
|
+
raise ArgumentError, 'param_types must contain only integer OIDs or nil'
|
|
175
175
|
end
|
|
176
176
|
|
|
177
177
|
def transition!(req, from, to)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
end
|
|
178
|
+
state = req.state
|
|
179
|
+
raise ProtocolError, "invalid request transition #{state.inspect} -> #{to.inspect}" unless state == from
|
|
181
180
|
|
|
182
181
|
req.state = to
|
|
183
182
|
end
|
|
@@ -205,14 +204,14 @@ module PgPipeline
|
|
|
205
204
|
end
|
|
206
205
|
|
|
207
206
|
def query_boundary!(req)
|
|
208
|
-
raise ProtocolError,
|
|
209
|
-
raise ProtocolError,
|
|
207
|
+
raise ProtocolError, 'query boundary before query result' unless req.result_seen
|
|
208
|
+
raise ProtocolError, 'duplicate query boundary' if req.query_boundary_seen
|
|
210
209
|
|
|
211
210
|
req.query_boundary_seen = true
|
|
212
211
|
end
|
|
213
212
|
|
|
214
213
|
def finish!(req)
|
|
215
|
-
raise ProtocolError,
|
|
214
|
+
raise ProtocolError, 'sync arrived before query boundary' unless req.query_boundary_seen
|
|
216
215
|
return if req.settled
|
|
217
216
|
|
|
218
217
|
req.settled = true
|
|
@@ -237,21 +236,23 @@ module PgPipeline
|
|
|
237
236
|
req.cancelled = true
|
|
238
237
|
clear_result(req.result)
|
|
239
238
|
req.result = nil
|
|
240
|
-
|
|
239
|
+
error = req.error
|
|
240
|
+
error.clear_result! if error.respond_to?(:clear_result!)
|
|
241
241
|
end
|
|
242
242
|
|
|
243
243
|
def wait(req)
|
|
244
244
|
park_waiter(req) unless req.settled
|
|
245
|
-
|
|
245
|
+
error = req.error
|
|
246
|
+
raise error if error
|
|
246
247
|
|
|
247
248
|
req.result
|
|
248
249
|
end
|
|
249
250
|
|
|
250
251
|
def park_waiter(req)
|
|
251
|
-
raise ProtocolError,
|
|
252
|
+
raise ProtocolError, 'request already has a waiter' if req.waiter
|
|
252
253
|
|
|
253
254
|
scheduler = Fiber.scheduler
|
|
254
|
-
raise Error,
|
|
255
|
+
raise Error, 'request wait requires an active Fiber scheduler' unless scheduler
|
|
255
256
|
|
|
256
257
|
waiter = Fiber.current
|
|
257
258
|
req.waiter = waiter
|
|
@@ -276,8 +277,8 @@ module PgPipeline
|
|
|
276
277
|
end
|
|
277
278
|
|
|
278
279
|
def assert_result_slot!(req)
|
|
279
|
-
raise ProtocolError,
|
|
280
|
-
raise ProtocolError,
|
|
280
|
+
raise ProtocolError, 'result arrived after query boundary' if req.query_boundary_seen
|
|
281
|
+
raise ProtocolError, 'multiple results for one pipeline unit' if req.result_seen
|
|
281
282
|
end
|
|
282
283
|
|
|
283
284
|
def clear_result(result)
|
|
@@ -5,7 +5,7 @@ module PgPipeline
|
|
|
5
5
|
class Semaphore
|
|
6
6
|
def initialize(limit)
|
|
7
7
|
@limit = Integer(limit)
|
|
8
|
-
raise ArgumentError,
|
|
8
|
+
raise ArgumentError, 'limit must be >= 0' if @limit.negative?
|
|
9
9
|
|
|
10
10
|
@available = @limit
|
|
11
11
|
@waiting = WaitList.new
|
|
@@ -71,7 +71,7 @@ module PgPipeline
|
|
|
71
71
|
def empty? = @head.nil?
|
|
72
72
|
|
|
73
73
|
def push(node)
|
|
74
|
-
raise ArgumentError,
|
|
74
|
+
raise ArgumentError, 'node already queued' if node.list
|
|
75
75
|
|
|
76
76
|
node.list = self
|
|
77
77
|
node.prev = @tail
|
|
@@ -108,7 +108,7 @@ module PgPipeline
|
|
|
108
108
|
@list = @prev = @next = nil
|
|
109
109
|
@granted = false
|
|
110
110
|
@scheduler = Fiber.scheduler
|
|
111
|
-
@waiter = {fiber: fiber, scheduler: @scheduler, ready: false, blocker: blocker, queued: true}
|
|
111
|
+
@waiter = { fiber: fiber, scheduler: @scheduler, ready: false, blocker: blocker, queued: true }
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def granted? = @granted
|
|
@@ -6,7 +6,7 @@ module PgPipeline
|
|
|
6
6
|
attr_reader :fiber, :name, :error
|
|
7
7
|
|
|
8
8
|
def self.spawn(name: nil, &block)
|
|
9
|
-
raise ArgumentError,
|
|
9
|
+
raise ArgumentError, 'block required' unless block
|
|
10
10
|
|
|
11
11
|
task = new(name: name)
|
|
12
12
|
task.__send__(:start, &block)
|
|
@@ -14,8 +14,13 @@ module PgPipeline
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def initialize(name: nil)
|
|
17
|
-
@fiber
|
|
18
|
-
@
|
|
17
|
+
@fiber = nil
|
|
18
|
+
@scheduler = nil
|
|
19
|
+
@blocker = nil
|
|
20
|
+
@result = nil
|
|
21
|
+
@error = nil
|
|
22
|
+
@done = false
|
|
23
|
+
@cancelled = false
|
|
19
24
|
|
|
20
25
|
@name = name
|
|
21
26
|
@waiters = []
|
|
@@ -24,7 +29,7 @@ module PgPipeline
|
|
|
24
29
|
def wait(timeout = nil)
|
|
25
30
|
unless @done
|
|
26
31
|
deadline = Runtime.deadline_for(timeout)
|
|
27
|
-
raise TimeoutError,
|
|
32
|
+
raise TimeoutError, 'task did not finish in time' unless join(deadline)
|
|
28
33
|
end
|
|
29
34
|
raise @error if @error
|
|
30
35
|
|
|
@@ -45,7 +50,7 @@ module PgPipeline
|
|
|
45
50
|
def cancelled? = @cancelled
|
|
46
51
|
|
|
47
52
|
def raise_if_cancelled!
|
|
48
|
-
raise Cancel,
|
|
53
|
+
raise Cancel, 'task stopped' if @cancelled
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
def enter_block(waiter)
|
|
@@ -68,7 +73,7 @@ module PgPipeline
|
|
|
68
73
|
this.__send__(:adopt_fiber, Fiber.current)
|
|
69
74
|
Thread.current[Runtime::CURRENT_TASK_KEY] = this
|
|
70
75
|
begin
|
|
71
|
-
this.__send__(:complete,
|
|
76
|
+
this.__send__(:complete, yield, nil)
|
|
72
77
|
rescue Cancel
|
|
73
78
|
this.__send__(:complete, nil, nil)
|
|
74
79
|
rescue Exception => e
|
|
@@ -89,7 +94,7 @@ module PgPipeline
|
|
|
89
94
|
return false unless fiber&.alive?
|
|
90
95
|
return false unless scheduler&.respond_to?(:fiber_interrupt)
|
|
91
96
|
|
|
92
|
-
result = scheduler.fiber_interrupt(fiber, Cancel.new(
|
|
97
|
+
result = scheduler.fiber_interrupt(fiber, Cancel.new('task stopped'))
|
|
93
98
|
result != false
|
|
94
99
|
rescue FiberError
|
|
95
100
|
false
|
|
@@ -119,7 +124,8 @@ module PgPipeline
|
|
|
119
124
|
end
|
|
120
125
|
|
|
121
126
|
def wake_waiters
|
|
122
|
-
pending
|
|
127
|
+
pending = @waiters
|
|
128
|
+
@waiters = []
|
|
123
129
|
pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
|
|
124
130
|
end
|
|
125
131
|
end
|
data/lib/pg_pipeline/runtime.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'timeout'
|
|
4
4
|
|
|
5
|
-
require_relative
|
|
5
|
+
require_relative 'errors'
|
|
6
6
|
|
|
7
7
|
module PgPipeline
|
|
8
8
|
module Runtime
|
|
@@ -14,7 +14,7 @@ module PgPipeline
|
|
|
14
14
|
|
|
15
15
|
CURRENT_TASK_KEY = :pg_pipeline_current_task
|
|
16
16
|
WAITER_KEY = :pg_pipeline_waiter
|
|
17
|
-
DEADLINE_MESSAGE =
|
|
17
|
+
DEADLINE_MESSAGE = 'execution expired'
|
|
18
18
|
|
|
19
19
|
MISSING_TIMEOUT_HOOK_WARNING = <<~MESSAGE
|
|
20
20
|
pg_pipeline: %s does not implement #timeout_after.
|
|
@@ -32,7 +32,7 @@ module PgPipeline
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def scheduler!
|
|
35
|
-
Fiber.scheduler or raise Error,
|
|
35
|
+
Fiber.scheduler or raise Error, 'operation requires an active Fiber scheduler'
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def native_timeouts?(target = Fiber.scheduler)
|
|
@@ -48,9 +48,10 @@ module PgPipeline
|
|
|
48
48
|
fiber: nil, scheduler: nil, ready: false, blocker: nil
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
current_blocker = waiter[:blocker]
|
|
52
|
+
if current_blocker
|
|
53
|
+
raise Error, "waiter already parked on #{current_blocker.class}; " \
|
|
54
|
+
'a fiber may only park in one place at a time'
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
waiter[:fiber] = Fiber.current
|
|
@@ -116,19 +117,19 @@ module PgPipeline
|
|
|
116
117
|
return nil if timeout.nil?
|
|
117
118
|
|
|
118
119
|
seconds = Float(timeout)
|
|
119
|
-
raise ArgumentError,
|
|
120
|
+
raise ArgumentError, 'timeout must be non-negative and finite' unless seconds.finite? && seconds >= 0
|
|
120
121
|
|
|
121
122
|
monotonic_now + seconds
|
|
122
123
|
end
|
|
123
124
|
|
|
124
|
-
def with_timeout(duration)
|
|
125
|
+
def with_timeout(duration, &block)
|
|
125
126
|
return yield if duration.nil?
|
|
126
127
|
|
|
127
128
|
seconds = Float(duration)
|
|
128
|
-
raise ArgumentError,
|
|
129
|
+
raise ArgumentError, 'timeout must be non-negative and finite' unless seconds.finite? && seconds >= 0
|
|
129
130
|
|
|
130
131
|
begin
|
|
131
|
-
arm_deadline(seconds)
|
|
132
|
+
arm_deadline(seconds, &block)
|
|
132
133
|
rescue Deadline => e
|
|
133
134
|
raise TimeoutError, e.message
|
|
134
135
|
end
|
|
@@ -136,9 +137,7 @@ module PgPipeline
|
|
|
136
137
|
|
|
137
138
|
def arm_deadline(seconds, &block)
|
|
138
139
|
target = Fiber.scheduler
|
|
139
|
-
if native_timeouts?(target)
|
|
140
|
-
return target.timeout_after(seconds, Deadline, DEADLINE_MESSAGE, &block)
|
|
141
|
-
end
|
|
140
|
+
return target.timeout_after(seconds, Deadline, DEADLINE_MESSAGE, &block) if native_timeouts?(target)
|
|
142
141
|
|
|
143
142
|
warn_missing_timeout_hook(target) if target
|
|
144
143
|
::Timeout.timeout(seconds, Deadline, DEADLINE_MESSAGE, &block)
|
|
@@ -154,7 +153,7 @@ module PgPipeline
|
|
|
154
153
|
end
|
|
155
154
|
end
|
|
156
155
|
|
|
157
|
-
require_relative
|
|
158
|
-
require_relative
|
|
159
|
-
require_relative
|
|
160
|
-
require_relative
|
|
156
|
+
require_relative 'runtime/notification'
|
|
157
|
+
require_relative 'runtime/queue'
|
|
158
|
+
require_relative 'runtime/semaphore'
|
|
159
|
+
require_relative 'runtime/task'
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'pg'
|
|
4
4
|
|
|
5
|
-
require_relative
|
|
5
|
+
require_relative 'errors'
|
|
6
6
|
|
|
7
7
|
module PgPipeline
|
|
8
8
|
class ServerCaps
|
|
@@ -59,7 +59,7 @@ module PgPipeline
|
|
|
59
59
|
return true if supported?(caps)
|
|
60
60
|
|
|
61
61
|
raise UnsupportedServerError,
|
|
62
|
-
|
|
62
|
+
'pg_pipeline requires libpq >= 14, PostgreSQL protocol v3, and ' \
|
|
63
63
|
"ruby-pg pipeline bindings; libpq=#{caps.libpq_version}, " \
|
|
64
64
|
"protocol=#{caps.protocol_version}, pipeline_api=#{caps.pipeline_api}"
|
|
65
65
|
end
|
data/lib/pg_pipeline/session.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'errors'
|
|
4
4
|
|
|
5
5
|
module PgPipeline
|
|
6
6
|
class Session
|
|
@@ -30,10 +30,10 @@ module PgPipeline
|
|
|
30
30
|
|
|
31
31
|
def ensure_active!(session)
|
|
32
32
|
conn = connection(session)
|
|
33
|
-
raise Error,
|
|
33
|
+
raise Error, 'session handle is no longer active' unless session.active? && conn
|
|
34
34
|
return if Fiber.current.equal?(session.owner_fiber)
|
|
35
35
|
|
|
36
|
-
raise Error,
|
|
36
|
+
raise Error, 'session handle is fiber-local and cannot be used from another fiber'
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def query(session, sql, params)
|
|
@@ -43,19 +43,21 @@ module PgPipeline
|
|
|
43
43
|
|
|
44
44
|
def exec(session, sql, params = nil)
|
|
45
45
|
ensure_active!(session)
|
|
46
|
+
conn = connection(session)
|
|
46
47
|
if params.nil?
|
|
47
|
-
|
|
48
|
+
conn.exec(sql)
|
|
48
49
|
else
|
|
49
|
-
|
|
50
|
+
conn.exec_params(sql, params)
|
|
50
51
|
end
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
def prepare(session, name, sql, param_types)
|
|
54
55
|
ensure_active!(session)
|
|
56
|
+
conn = connection(session)
|
|
55
57
|
if param_types
|
|
56
|
-
|
|
58
|
+
conn.prepare(name, sql, param_types)
|
|
57
59
|
else
|
|
58
|
-
|
|
60
|
+
conn.prepare(name, sql)
|
|
59
61
|
end
|
|
60
62
|
end
|
|
61
63
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'errors'
|
|
4
4
|
|
|
5
5
|
module PgPipeline
|
|
6
6
|
module SessionGuard
|
|
@@ -9,27 +9,27 @@ module PgPipeline
|
|
|
9
9
|
VALID_MODES = %i[default strict].freeze
|
|
10
10
|
ALLOWED_LEADING = %w[select insert update delete merge values with].freeze
|
|
11
11
|
FORBIDDEN_PATTERNS = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
'set_config' => /\bset_config\s*\(/i,
|
|
13
|
+
'setseed' => /\bsetseed\s*\(/i,
|
|
14
|
+
'currval' => /\bcurrval\s*\(/i,
|
|
15
|
+
'lastval' => /\blastval\s*\(/i,
|
|
16
|
+
'session-advisory-lock' => /\bpg_(?:try_)?advisory_lock(?:_shared)?\s*\(/i,
|
|
17
|
+
'session-advisory-unlock' => /\bpg_advisory_unlock(?:_shared|_all)?\s*\(/i,
|
|
18
|
+
'select-into-temp' => /\binto\s+(?:(?:global|local)\s+)?temp(?:orary)?\b/i,
|
|
19
|
+
'select-into-pg-temp' => /\binto\s+(?:table\s+)?pg_temp(?:_\d+)?\s*\./i
|
|
20
20
|
}.freeze
|
|
21
21
|
STRICT_FORBIDDEN = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
'nextval' => /\bnextval\s*\(/i,
|
|
23
|
+
'setval' => /\bsetval\s*\(/i,
|
|
24
|
+
'setseed' => /\bsetseed\s*\(/i,
|
|
25
|
+
'set_config' => /\bset_config\s*\(/i,
|
|
26
|
+
'session-advisory-lock' => /\bpg_(?:try_)?advisory_lock(?:_shared)?\s*\(/i,
|
|
27
|
+
'session-advisory-unlock' => /\bpg_advisory_unlock(?:_shared|_all)?\s*\(/i,
|
|
28
|
+
'pg_export_snapshot' => /\bpg_export_snapshot\s*\(/i
|
|
29
29
|
}.freeze
|
|
30
30
|
PATTERN_PREFILTER = /\binto\b/i
|
|
31
31
|
DOLLAR_QUOTE_TAG = /\A\$(?:(?:[A-Za-z_]|[^\x00-\x7F])(?:[A-Za-z0-9_]|[^\x00-\x7F])*)?\$/
|
|
32
|
-
NEEDS_MASK =
|
|
32
|
+
NEEDS_MASK = %r{['"]|--|/\*|\$(?:(?:[A-Za-z_]|[^\x00-\x7F])(?:[A-Za-z0-9_]|[^\x00-\x7F])*)?\$}
|
|
33
33
|
LEADING_KEYWORD = /\A\s*([a-zA-Z_]+)/
|
|
34
34
|
WHITESPACE_BYTES = [9, 10, 11, 12, 13, 32].freeze
|
|
35
35
|
|
|
@@ -43,7 +43,7 @@ module PgPipeline
|
|
|
43
43
|
|
|
44
44
|
raise UnsafeMultiplexError,
|
|
45
45
|
"refusing to multiplex SQL (#{reason}); the shared path only accepts " \
|
|
46
|
-
|
|
46
|
+
'session-neutral operations. Use Client#session for session work or ' \
|
|
47
47
|
"Client#transaction for an explicit transaction.\n" \
|
|
48
48
|
" offending SQL: #{sql.to_s.strip[0, 160]}"
|
|
49
49
|
end
|
|
@@ -75,20 +75,29 @@ module PgPipeline
|
|
|
75
75
|
code = NEEDS_MASK.match?(sql) ? code_only(sql) : sql
|
|
76
76
|
lead = code[LEADING_KEYWORD, 1]&.downcase
|
|
77
77
|
|
|
78
|
-
return
|
|
78
|
+
return 'empty' unless lead
|
|
79
79
|
return "leading:#{lead}" unless ALLOWED_LEADING.include?(lead)
|
|
80
|
-
return
|
|
81
|
-
return nil unless code.include?(
|
|
80
|
+
return 'multiple-statements' if multiple_statements?(code)
|
|
81
|
+
return nil unless code.include?('(') || code.match?(PATTERN_PREFILTER)
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
forbidden_reason(code) || strict_forbidden_reason(code, mode)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def forbidden_reason(code)
|
|
87
|
+
pattern_reason(code, FORBIDDEN_PATTERNS)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def strict_forbidden_reason(code, mode)
|
|
91
|
+
return unless mode == :strict
|
|
92
|
+
|
|
93
|
+
reason = pattern_reason(code, STRICT_FORBIDDEN)
|
|
94
|
+
"strict:#{reason}" if reason
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def pattern_reason(code, patterns)
|
|
98
|
+
patterns.each do |name, pattern|
|
|
84
99
|
return name if code.match?(pattern)
|
|
85
100
|
end
|
|
86
|
-
if mode == :strict
|
|
87
|
-
STRICT_FORBIDDEN.each do |name, pattern|
|
|
88
|
-
return "strict:#{name}" if code.match?(pattern)
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
101
|
nil
|
|
93
102
|
end
|
|
94
103
|
|
|
@@ -96,7 +105,7 @@ module PgPipeline
|
|
|
96
105
|
normalized = mode.respond_to?(:to_sym) ? mode.to_sym : mode
|
|
97
106
|
return normalized if VALID_MODES.include?(normalized)
|
|
98
107
|
|
|
99
|
-
raise ArgumentError, "guard must be one of: #{VALID_MODES.map(&:inspect).join(
|
|
108
|
+
raise ArgumentError, "guard must be one of: #{VALID_MODES.map(&:inspect).join(', ')}"
|
|
100
109
|
end
|
|
101
110
|
|
|
102
111
|
def code_only(sql)
|
|
@@ -104,43 +113,19 @@ module PgPipeline
|
|
|
104
113
|
size = source.bytesize
|
|
105
114
|
output = String.new(capacity: size, encoding: Encoding::BINARY)
|
|
106
115
|
index = 0
|
|
107
|
-
block_depth = 0
|
|
108
116
|
|
|
109
117
|
while index < size
|
|
110
118
|
byte = source.getbyte(index)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if byte == 47 && nxt == 42
|
|
114
|
-
block_depth += 1
|
|
115
|
-
output << " "
|
|
116
|
-
index += 2
|
|
117
|
-
elsif byte == 42 && nxt == 47
|
|
118
|
-
block_depth -= 1
|
|
119
|
-
output << " "
|
|
120
|
-
index += 2
|
|
121
|
-
else
|
|
122
|
-
output << (byte == 10 ? 10 : 32)
|
|
123
|
-
index += 1
|
|
124
|
-
end
|
|
125
|
-
next
|
|
126
|
-
end
|
|
119
|
+
next_index = index + 1
|
|
120
|
+
nxt = next_index < size ? source.getbyte(next_index) : nil
|
|
127
121
|
|
|
128
122
|
if byte == 45 && nxt == 45
|
|
129
|
-
|
|
130
|
-
if newline
|
|
131
|
-
output << (" " * (newline - index)) << "\n"
|
|
132
|
-
index = newline + 1
|
|
133
|
-
else
|
|
134
|
-
output << (" " * (size - index))
|
|
135
|
-
break
|
|
136
|
-
end
|
|
123
|
+
index = mask_line_comment(source, output, index)
|
|
137
124
|
next
|
|
138
125
|
end
|
|
139
126
|
|
|
140
127
|
if byte == 47 && nxt == 42
|
|
141
|
-
|
|
142
|
-
output << " "
|
|
143
|
-
index += 2
|
|
128
|
+
index = mask_block_comment(source, output, index)
|
|
144
129
|
next
|
|
145
130
|
end
|
|
146
131
|
|
|
@@ -155,12 +140,8 @@ module PgPipeline
|
|
|
155
140
|
end
|
|
156
141
|
|
|
157
142
|
if byte == 36
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if tag
|
|
161
|
-
closing = source.index(tag, index + tag.bytesize)
|
|
162
|
-
finish = closing ? closing + tag.bytesize : size
|
|
163
|
-
output << (" " * (finish - index))
|
|
143
|
+
finish = mask_dollar_quote(source, output, index, size)
|
|
144
|
+
if finish
|
|
164
145
|
index = finish
|
|
165
146
|
next
|
|
166
147
|
end
|
|
@@ -173,29 +154,79 @@ module PgPipeline
|
|
|
173
154
|
output
|
|
174
155
|
end
|
|
175
156
|
|
|
157
|
+
def mask_line_comment(source, output, index)
|
|
158
|
+
newline = source.index("\n", index + 2)
|
|
159
|
+
if newline
|
|
160
|
+
output << (' ' * (newline - index)) << "\n"
|
|
161
|
+
newline + 1
|
|
162
|
+
else
|
|
163
|
+
output << (' ' * (source.bytesize - index))
|
|
164
|
+
source.bytesize
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def mask_block_comment(source, output, index)
|
|
169
|
+
depth = 1
|
|
170
|
+
index = append_comment_delimiter(output, index)
|
|
171
|
+
|
|
172
|
+
while index < source.bytesize
|
|
173
|
+
byte = source.getbyte(index)
|
|
174
|
+
nxt = source.getbyte(index + 1)
|
|
175
|
+
if byte == 47 && nxt == 42
|
|
176
|
+
depth += 1
|
|
177
|
+
index = append_comment_delimiter(output, index)
|
|
178
|
+
elsif byte == 42 && nxt == 47
|
|
179
|
+
depth -= 1
|
|
180
|
+
index = append_comment_delimiter(output, index)
|
|
181
|
+
break if depth.zero?
|
|
182
|
+
else
|
|
183
|
+
output << (byte == 10 ? 10 : 32)
|
|
184
|
+
index += 1
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
index
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def append_comment_delimiter(output, index)
|
|
192
|
+
output << ' '
|
|
193
|
+
index + 2
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def mask_dollar_quote(source, output, index, size)
|
|
197
|
+
remainder = source.byteslice(index, size - index)
|
|
198
|
+
tag = DOLLAR_QUOTE_TAG.match(remainder)&.[](0)
|
|
199
|
+
return unless tag
|
|
200
|
+
|
|
201
|
+
closing = source.index(tag, index + tag.bytesize)
|
|
202
|
+
finish = closing ? closing + tag.bytesize : size
|
|
203
|
+
output << (' ' * (finish - index))
|
|
204
|
+
finish
|
|
205
|
+
end
|
|
206
|
+
|
|
176
207
|
def mask_quoted(source, output, index, quote_byte, escape_backslash:)
|
|
177
|
-
output <<
|
|
208
|
+
output << ' '
|
|
178
209
|
index += 1
|
|
179
210
|
|
|
180
211
|
while index < source.bytesize
|
|
181
212
|
byte = source.getbyte(index)
|
|
182
213
|
if escape_backslash && byte == 92
|
|
183
|
-
output <<
|
|
214
|
+
output << ' '
|
|
184
215
|
index += 1
|
|
185
216
|
if index < source.bytesize
|
|
186
|
-
output << (source.getbyte(index) == 10 ? "\n" :
|
|
217
|
+
output << (source.getbyte(index) == 10 ? "\n" : ' ')
|
|
187
218
|
index += 1
|
|
188
219
|
end
|
|
189
220
|
elsif byte == quote_byte
|
|
190
221
|
if source.getbyte(index + 1) == quote_byte
|
|
191
|
-
output <<
|
|
222
|
+
output << ' '
|
|
192
223
|
index += 2
|
|
193
224
|
else
|
|
194
|
-
output <<
|
|
225
|
+
output << ' '
|
|
195
226
|
return index + 1
|
|
196
227
|
end
|
|
197
228
|
else
|
|
198
|
-
output << (byte == 10 ? "\n" :
|
|
229
|
+
output << (byte == 10 ? "\n" : ' ')
|
|
199
230
|
index += 1
|
|
200
231
|
end
|
|
201
232
|
end
|
|
@@ -207,15 +238,19 @@ module PgPipeline
|
|
|
207
238
|
return false if quote_index.zero?
|
|
208
239
|
|
|
209
240
|
marker = source.getbyte(quote_index - 1)
|
|
210
|
-
return false unless
|
|
241
|
+
return false unless [69, 101].include?(marker)
|
|
211
242
|
|
|
212
243
|
before = quote_index >= 2 ? source.getbyte(quote_index - 2) : nil
|
|
213
|
-
|
|
244
|
+
!sql_identifier_byte?(before)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def sql_identifier_byte?(byte)
|
|
248
|
+
byte && (byte.between?(48, 57) || byte.between?(65, 90) || byte.between?(97, 122) || byte == 95)
|
|
214
249
|
end
|
|
215
|
-
private_class_method :escape_string_prefix?
|
|
250
|
+
private_class_method :escape_string_prefix?, :sql_identifier_byte?
|
|
216
251
|
|
|
217
252
|
def multiple_statements?(code)
|
|
218
|
-
first = code.index(
|
|
253
|
+
first = code.index(';')
|
|
219
254
|
return false unless first
|
|
220
255
|
|
|
221
256
|
index = first + 1
|