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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "errors"
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, "params must be an Array" unless params.is_a?(Array)
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, "statement_name must not be empty" if value.empty?
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, "param_types must be an Array or nil" unless param_types.is_a?(Array)
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, "param_types must contain only integer OIDs or nil"
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
- unless req.state == from
179
- raise ProtocolError, "invalid request transition #{req.state.inspect} -> #{to.inspect}"
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, "query boundary before query result" unless req.result_seen
209
- raise ProtocolError, "duplicate query boundary" if req.query_boundary_seen
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, "sync arrived before query boundary" unless req.query_boundary_seen
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
- req.error.clear_result! if req.error.respond_to?(:clear_result!)
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
- raise req.error if req.error
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, "request already has a waiter" if req.waiter
252
+ raise ProtocolError, 'request already has a waiter' if req.waiter
252
253
 
253
254
  scheduler = Fiber.scheduler
254
- raise Error, "request wait requires an active Fiber scheduler" unless scheduler
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, "result arrived after query boundary" if req.query_boundary_seen
280
- raise ProtocolError, "multiple results for one pipeline unit" if req.result_seen
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)
@@ -28,7 +28,8 @@ module PgPipeline
28
28
  end
29
29
 
30
30
  def signal_all
31
- pending, @waiters = @waiters, []
31
+ pending = @waiters
32
+ @waiters = []
32
33
  pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
33
34
  nil
34
35
  end
@@ -5,7 +5,7 @@ module PgPipeline
5
5
  class Semaphore
6
6
  def initialize(limit)
7
7
  @limit = Integer(limit)
8
- raise ArgumentError, "limit must be >= 0" if @limit.negative?
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, "node already queued" if node.list
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, "block required" unless block
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, @scheduler, @blocker, @result, @error = nil, nil, nil, nil, nil
18
- @done, @cancelled = false, false
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, "task did not finish in time" unless join(deadline)
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, "task stopped" if @cancelled
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, block.call, nil)
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("task stopped"))
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, @waiters = @waiters, []
127
+ pending = @waiters
128
+ @waiters = []
123
129
  pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
124
130
  end
125
131
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "timeout"
3
+ require 'timeout'
4
4
 
5
- require_relative "errors"
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 = "execution expired"
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, "operation requires an active Fiber scheduler"
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
- if waiter[:blocker]
52
- raise Error, "waiter already parked on #{waiter[:blocker].class}; " \
53
- "a fiber may only park in one place at a time"
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, "timeout must be non-negative and finite" unless seconds.finite? && seconds >= 0
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, "timeout must be non-negative and finite" unless seconds.finite? && seconds >= 0
129
+ raise ArgumentError, 'timeout must be non-negative and finite' unless seconds.finite? && seconds >= 0
129
130
 
130
131
  begin
131
- arm_deadline(seconds) { yield }
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 "runtime/notification"
158
- require_relative "runtime/queue"
159
- require_relative "runtime/semaphore"
160
- require_relative "runtime/task"
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 "pg"
3
+ require 'pg'
4
4
 
5
- require_relative "errors"
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
- "pg_pipeline requires libpq >= 14, PostgreSQL protocol v3, and " \
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "errors"
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, "session handle is no longer active" unless session.active? && conn
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, "session handle is fiber-local and cannot be used from another fiber"
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
- connection(session).exec(sql)
48
+ conn.exec(sql)
48
49
  else
49
- connection(session).exec_params(sql, params)
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
- connection(session).prepare(name, sql, param_types)
58
+ conn.prepare(name, sql, param_types)
57
59
  else
58
- connection(session).prepare(name, sql)
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 "errors"
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
- "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
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
- "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
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 = /['"]|--|\/\*|\$(?:(?:[A-Za-z_]|[^\x00-\x7F])(?:[A-Za-z0-9_]|[^\x00-\x7F])*)?\$/
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
- "session-neutral operations. Use Client#session for session work or " \
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 "empty" unless lead
78
+ return 'empty' unless lead
79
79
  return "leading:#{lead}" unless ALLOWED_LEADING.include?(lead)
80
- return "multiple-statements" if multiple_statements?(code)
81
- return nil unless code.include?("(") || code.match?(PATTERN_PREFILTER)
80
+ return 'multiple-statements' if multiple_statements?(code)
81
+ return nil unless code.include?('(') || code.match?(PATTERN_PREFILTER)
82
82
 
83
- FORBIDDEN_PATTERNS.each do |name, pattern|
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
- nxt = index + 1 < size ? source.getbyte(index + 1) : nil
112
- if block_depth.positive?
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
- newline = source.index("\n", index + 2)
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
- block_depth = 1
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
- remainder = source.byteslice(index, size - index)
159
- tag = DOLLAR_QUOTE_TAG.match(remainder)&.[](0)
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 marker == 69 || marker == 101
241
+ return false unless [69, 101].include?(marker)
211
242
 
212
243
  before = quote_index >= 2 ? source.getbyte(quote_index - 2) : nil
213
- before.nil? || !(before.between?(48, 57) || before.between?(65, 90) || before.between?(97, 122) || before == 95)
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