pg_pipeline 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +156 -0
- data/DESIGN.md +298 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/lib/pg_pipeline/bounded_queue.rb +102 -0
- data/lib/pg_pipeline/client.rb +180 -0
- data/lib/pg_pipeline/connection_driver.rb +502 -0
- data/lib/pg_pipeline/errors.rb +27 -0
- data/lib/pg_pipeline/pool.rb +420 -0
- data/lib/pg_pipeline/request.rb +146 -0
- data/lib/pg_pipeline/server_caps.rb +83 -0
- data/lib/pg_pipeline/session.rb +71 -0
- data/lib/pg_pipeline/session_guard.rb +213 -0
- data/lib/pg_pipeline/transaction.rb +92 -0
- data/lib/pg_pipeline/version.rb +5 -0
- data/lib/pg_pipeline.rb +15 -0
- metadata +155 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module PgPipeline
|
|
6
|
+
module SessionGuard
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
VALID_MODES = %i[default strict].freeze
|
|
10
|
+
ALLOWED_LEADING = %w[select insert update delete merge values with].freeze
|
|
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
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
STRICT_FORBIDDEN = {
|
|
23
|
+
"nextval" => /\bnextval\s*\(/i,
|
|
24
|
+
"setval" => /\bsetval\s*\(/i,
|
|
25
|
+
"setseed" => /\bsetseed\s*\(/i,
|
|
26
|
+
"set_config" => /\bset_config\s*\(/i,
|
|
27
|
+
"session-advisory-lock" => /\bpg_(?:try_)?advisory_lock(?:_shared)?\s*\(/i,
|
|
28
|
+
"session-advisory-unlock" => /\bpg_advisory_unlock(?:_shared|_all)?\s*\(/i,
|
|
29
|
+
"pg_export_snapshot" => /\bpg_export_snapshot\s*\(/i
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
def assert_multiplexable!(sql, mode: :default)
|
|
33
|
+
reason = unsafe_reason(sql, mode: mode)
|
|
34
|
+
return true unless reason
|
|
35
|
+
|
|
36
|
+
raise UnsafeMultiplexError,
|
|
37
|
+
"refusing to multiplex SQL (#{reason}); the shared path only accepts " \
|
|
38
|
+
"session-neutral operations. Use Client#session for session work or " \
|
|
39
|
+
"Client#transaction for an explicit transaction.\n" \
|
|
40
|
+
" offending SQL: #{sql.to_s.strip[0, 160]}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
GUARD_CACHE_LIMIT = 2048
|
|
44
|
+
|
|
45
|
+
def unsafe_reason(sql, mode: :default)
|
|
46
|
+
mode = normalize_mode!(mode)
|
|
47
|
+
key = sql.to_s
|
|
48
|
+
cache = guard_cache[mode]
|
|
49
|
+
return cache[key] if cache.key?(key)
|
|
50
|
+
|
|
51
|
+
reason = compute_unsafe_reason(key, mode)
|
|
52
|
+
cache.clear if cache.size >= GUARD_CACHE_LIMIT
|
|
53
|
+
cache[key] = reason
|
|
54
|
+
reason
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def guard_cache
|
|
58
|
+
@guard_cache ||= { default: {}, strict: {} }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def compute_unsafe_reason(sql, mode)
|
|
62
|
+
code = code_only(sql)
|
|
63
|
+
lead = code[/\A\s*([a-zA-Z_]+)/, 1]&.downcase
|
|
64
|
+
|
|
65
|
+
return "empty" unless lead
|
|
66
|
+
return "leading:#{lead}" unless ALLOWED_LEADING.include?(lead)
|
|
67
|
+
return "multiple-statements" if multiple_statements?(code)
|
|
68
|
+
|
|
69
|
+
FORBIDDEN_PATTERNS.each do |name, pattern|
|
|
70
|
+
return name if code.match?(pattern)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if mode == :strict
|
|
74
|
+
STRICT_FORBIDDEN.each do |name, pattern|
|
|
75
|
+
return "strict:#{name}" if code.match?(pattern)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def normalize_mode!(mode)
|
|
83
|
+
normalized = mode.respond_to?(:to_sym) ? mode.to_sym : mode
|
|
84
|
+
return normalized if VALID_MODES.include?(normalized)
|
|
85
|
+
|
|
86
|
+
raise ArgumentError, "guard must be one of: #{VALID_MODES.map(&:inspect).join(", ")}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def code_only(sql)
|
|
90
|
+
source = sql.to_s.b
|
|
91
|
+
output = String.new(capacity: source.bytesize, encoding: Encoding::BINARY)
|
|
92
|
+
index = 0
|
|
93
|
+
block_depth = 0
|
|
94
|
+
|
|
95
|
+
while index < source.bytesize
|
|
96
|
+
if block_depth.positive?
|
|
97
|
+
if source.byteslice(index, 2) == "/*"
|
|
98
|
+
block_depth += 1
|
|
99
|
+
output << " "
|
|
100
|
+
index += 2
|
|
101
|
+
elsif source.byteslice(index, 2) == "*/"
|
|
102
|
+
block_depth -= 1
|
|
103
|
+
output << " "
|
|
104
|
+
index += 2
|
|
105
|
+
else
|
|
106
|
+
output << (source.getbyte(index) == 10 ? "\n" : " ")
|
|
107
|
+
index += 1
|
|
108
|
+
end
|
|
109
|
+
next
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if source.byteslice(index, 2) == "--"
|
|
113
|
+
newline = source.index("\n", index + 2)
|
|
114
|
+
if newline
|
|
115
|
+
output << " " * (newline - index) << "\n"
|
|
116
|
+
index = newline + 1
|
|
117
|
+
else
|
|
118
|
+
output << " " * (source.bytesize - index)
|
|
119
|
+
break
|
|
120
|
+
end
|
|
121
|
+
next
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if source.byteslice(index, 2) == "/*"
|
|
125
|
+
block_depth = 1
|
|
126
|
+
output << " "
|
|
127
|
+
index += 2
|
|
128
|
+
next
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
byte = source.getbyte(index)
|
|
132
|
+
if byte == 39
|
|
133
|
+
index = mask_quoted(source, output, index, 39, escape_backslash: escape_string_prefix?(source, index))
|
|
134
|
+
next
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if byte == 34
|
|
138
|
+
index = mask_quoted(source, output, index, 34, escape_backslash: false)
|
|
139
|
+
next
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if byte == 36
|
|
143
|
+
remainder = source.byteslice(index, source.bytesize - index)
|
|
144
|
+
tag = remainder.match(/\A\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/)&.[](0)
|
|
145
|
+
if tag
|
|
146
|
+
closing = source.index(tag, index + tag.bytesize)
|
|
147
|
+
finish = closing ? closing + tag.bytesize : source.bytesize
|
|
148
|
+
output << " " * (finish - index)
|
|
149
|
+
index = finish
|
|
150
|
+
next
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
output << source.getbyte(index)
|
|
155
|
+
index += 1
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
output
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def mask_quoted(source, output, index, quote_byte, escape_backslash:)
|
|
162
|
+
output << " "
|
|
163
|
+
index += 1
|
|
164
|
+
|
|
165
|
+
while index < source.bytesize
|
|
166
|
+
byte = source.getbyte(index)
|
|
167
|
+
|
|
168
|
+
if escape_backslash && byte == 92
|
|
169
|
+
output << " "
|
|
170
|
+
index += 1
|
|
171
|
+
if index < source.bytesize
|
|
172
|
+
output << (source.getbyte(index) == 10 ? "\n" : " ")
|
|
173
|
+
index += 1
|
|
174
|
+
end
|
|
175
|
+
elsif byte == quote_byte
|
|
176
|
+
if source.getbyte(index + 1) == quote_byte
|
|
177
|
+
output << " "
|
|
178
|
+
index += 2
|
|
179
|
+
else
|
|
180
|
+
output << " "
|
|
181
|
+
return index + 1
|
|
182
|
+
end
|
|
183
|
+
else
|
|
184
|
+
output << (byte == 10 ? "\n" : " ")
|
|
185
|
+
index += 1
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
index
|
|
190
|
+
end
|
|
191
|
+
private_class_method :mask_quoted
|
|
192
|
+
|
|
193
|
+
def escape_string_prefix?(source, quote_index)
|
|
194
|
+
return false if quote_index.zero?
|
|
195
|
+
|
|
196
|
+
marker = source.getbyte(quote_index - 1)
|
|
197
|
+
return false unless marker == 69 || marker == 101
|
|
198
|
+
|
|
199
|
+
before = quote_index >= 2 ? source.getbyte(quote_index - 2) : nil
|
|
200
|
+
before.nil? || !(before.between?(48, 57) || before.between?(65, 90) || before.between?(97, 122) || before == 95)
|
|
201
|
+
end
|
|
202
|
+
private_class_method :escape_string_prefix?
|
|
203
|
+
|
|
204
|
+
def multiple_statements?(code)
|
|
205
|
+
semicolons = []
|
|
206
|
+
code.each_char.with_index { |char, i| semicolons << i if char == ";" }
|
|
207
|
+
return false if semicolons.empty?
|
|
208
|
+
|
|
209
|
+
last_non_space = code.rstrip.length - 1
|
|
210
|
+
semicolons.length > 1 || semicolons.first != last_non_space
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pg"
|
|
4
|
+
|
|
5
|
+
require_relative "session"
|
|
6
|
+
|
|
7
|
+
module PgPipeline
|
|
8
|
+
class Transaction < Session
|
|
9
|
+
attr_accessor :open, :savepoint_seq
|
|
10
|
+
|
|
11
|
+
def initialize(conn)
|
|
12
|
+
super
|
|
13
|
+
@open = false
|
|
14
|
+
@savepoint_seq = 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run(&block)
|
|
18
|
+
TransactionOps.run(self, &block)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def savepoint(name = nil, &block)
|
|
22
|
+
TransactionOps.savepoint(self, name, &block)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def open? = @open
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module TransactionOps
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
def run(tx)
|
|
32
|
+
SessionOps.ensure_active!(tx)
|
|
33
|
+
raise Error, "transaction is already open" if tx.open
|
|
34
|
+
|
|
35
|
+
conn = SessionOps.connection(tx)
|
|
36
|
+
conn.exec("BEGIN")
|
|
37
|
+
tx.open = true
|
|
38
|
+
begin
|
|
39
|
+
result = yield tx
|
|
40
|
+
conn.exec("COMMIT")
|
|
41
|
+
tx.open = false
|
|
42
|
+
result
|
|
43
|
+
rescue Exception
|
|
44
|
+
# Only roll back the transaction we opened. Early guard errors
|
|
45
|
+
# ("already open", inactive handle) must not hit this path.
|
|
46
|
+
rollback_quietly(tx)
|
|
47
|
+
raise
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def savepoint(tx, name)
|
|
52
|
+
SessionOps.ensure_active!(tx)
|
|
53
|
+
raise Error, "savepoint requires an open transaction" unless tx.open
|
|
54
|
+
|
|
55
|
+
conn = SessionOps.connection(tx)
|
|
56
|
+
tx.savepoint_seq += 1
|
|
57
|
+
point = name || "pgp_sp_#{tx.savepoint_seq}"
|
|
58
|
+
ident = conn.quote_ident(point)
|
|
59
|
+
|
|
60
|
+
conn.exec("SAVEPOINT #{ident}")
|
|
61
|
+
begin
|
|
62
|
+
result = yield tx
|
|
63
|
+
conn.exec("RELEASE SAVEPOINT #{ident}")
|
|
64
|
+
result
|
|
65
|
+
rescue Exception
|
|
66
|
+
rollback_to_savepoint(tx, ident)
|
|
67
|
+
raise
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def rollback_to_savepoint(tx, ident)
|
|
72
|
+
conn = SessionOps.connection(tx)
|
|
73
|
+
return unless conn
|
|
74
|
+
|
|
75
|
+
conn.exec("ROLLBACK TO SAVEPOINT #{ident}")
|
|
76
|
+
conn.exec("RELEASE SAVEPOINT #{ident}")
|
|
77
|
+
rescue PG::Error
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def rollback_quietly(tx)
|
|
82
|
+
return unless tx.open
|
|
83
|
+
|
|
84
|
+
conn = SessionOps.connection(tx)
|
|
85
|
+
conn&.exec("ROLLBACK")
|
|
86
|
+
rescue PG::Error
|
|
87
|
+
nil
|
|
88
|
+
ensure
|
|
89
|
+
tx.open = false
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/pg_pipeline.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pg_pipeline/version"
|
|
4
|
+
require_relative "pg_pipeline/errors"
|
|
5
|
+
require_relative "pg_pipeline/server_caps"
|
|
6
|
+
require_relative "pg_pipeline/session_guard"
|
|
7
|
+
require_relative "pg_pipeline/session"
|
|
8
|
+
require_relative "pg_pipeline/transaction"
|
|
9
|
+
require_relative "pg_pipeline/request"
|
|
10
|
+
require_relative "pg_pipeline/connection_driver"
|
|
11
|
+
require_relative "pg_pipeline/pool"
|
|
12
|
+
require_relative "pg_pipeline/client"
|
|
13
|
+
|
|
14
|
+
module PgPipeline
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pg_pipeline
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Roman Hajdarov
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: async
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.42'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.42'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: pg
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.5'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '2'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '1.5'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: rake
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '13.0'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '13.0'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: rspec
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.13'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '3.13'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: ruby-prof
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '1.7'
|
|
81
|
+
type: :development
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '1.7'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: stackprof
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0.2'
|
|
95
|
+
type: :development
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '0.2'
|
|
102
|
+
description: |
|
|
103
|
+
A driver-adjacent Ruby control-plane over ruby-pg/libpq. It multiplexes
|
|
104
|
+
independent, session-neutral extended-protocol operations from many Async
|
|
105
|
+
fibers onto a small number of PostgreSQL connections while keeping explicit
|
|
106
|
+
transactions and session-changing work on exclusive pinned connections.
|
|
107
|
+
Control-plane only: all wire work stays in libpq. Zero lines of C.
|
|
108
|
+
email:
|
|
109
|
+
- romanhajdarov@gmail.com
|
|
110
|
+
executables: []
|
|
111
|
+
extensions: []
|
|
112
|
+
extra_rdoc_files: []
|
|
113
|
+
files:
|
|
114
|
+
- CHANGELOG.md
|
|
115
|
+
- DESIGN.md
|
|
116
|
+
- LICENSE.txt
|
|
117
|
+
- README.md
|
|
118
|
+
- lib/pg_pipeline.rb
|
|
119
|
+
- lib/pg_pipeline/bounded_queue.rb
|
|
120
|
+
- lib/pg_pipeline/client.rb
|
|
121
|
+
- lib/pg_pipeline/connection_driver.rb
|
|
122
|
+
- lib/pg_pipeline/errors.rb
|
|
123
|
+
- lib/pg_pipeline/pool.rb
|
|
124
|
+
- lib/pg_pipeline/request.rb
|
|
125
|
+
- lib/pg_pipeline/server_caps.rb
|
|
126
|
+
- lib/pg_pipeline/session.rb
|
|
127
|
+
- lib/pg_pipeline/session_guard.rb
|
|
128
|
+
- lib/pg_pipeline/transaction.rb
|
|
129
|
+
- lib/pg_pipeline/version.rb
|
|
130
|
+
homepage: https://github.com/roman-haidarov/pg_pipeline
|
|
131
|
+
licenses:
|
|
132
|
+
- MIT
|
|
133
|
+
metadata:
|
|
134
|
+
homepage_uri: https://github.com/roman-haidarov/pg_pipeline
|
|
135
|
+
source_code_uri: https://github.com/roman-haidarov/pg_pipeline/tree/main
|
|
136
|
+
changelog_uri: https://github.com/roman-haidarov/pg_pipeline/blob/main/CHANGELOG.md
|
|
137
|
+
bug_tracker_uri: https://github.com/roman-haidarov/pg_pipeline/issues
|
|
138
|
+
rdoc_options: []
|
|
139
|
+
require_paths:
|
|
140
|
+
- lib
|
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '3.3'
|
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - ">="
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '0'
|
|
151
|
+
requirements: []
|
|
152
|
+
rubygems_version: 3.6.7
|
|
153
|
+
specification_version: 4
|
|
154
|
+
summary: Async-native PostgreSQL pipeline multiplexing on top of ruby-pg
|
|
155
|
+
test_files: []
|