pg_pipeline 0.3.1 → 0.3.3
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 +62 -0
- data/README.md +21 -0
- data/lib/pg_pipeline/client.rb +10 -4
- data/lib/pg_pipeline/connection_driver.rb +62 -35
- data/lib/pg_pipeline/pool.rb +124 -77
- data/lib/pg_pipeline/prepared_statement.rb +7 -4
- data/lib/pg_pipeline/request.rb +3 -1
- data/lib/pg_pipeline/runtime/notification.rb +12 -7
- data/lib/pg_pipeline/runtime/semaphore.rb +6 -5
- data/lib/pg_pipeline/runtime/task.rb +9 -6
- data/lib/pg_pipeline/runtime.rb +74 -12
- data/lib/pg_pipeline/type_maps.rb +89 -0
- data/lib/pg_pipeline/version.rb +1 -1
- data/lib/pg_pipeline.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3bb4583fbf47130e8b86eec8f44c70577f053c9cacd1e9774a09a5b7a5d3999
|
|
4
|
+
data.tar.gz: '099f44877fde9db5faa63a776e31ecb6d1b56cc155dd08fd0b6dff52252a719e'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ddc6fa628e441b5cf744a3521e0ca90a923cf732be59662c6cee9df5f522ad69319628d6ee98e18711baa28d8d42eef7585c0af6239cb9a8f4f921212338d014
|
|
7
|
+
data.tar.gz: 75e56315d776d60cadf07b675e048fdb674f06bc8b67fe960e95be4d7ed8bd89008eb25a8828eb96655994ed83fa23f392a541a73b77dbc4f113dc28261c815a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.3] - 2026-08-19
|
|
4
|
+
|
|
5
|
+
Opt-in typed result decoding for multiplexed prepared statements. Default
|
|
6
|
+
behaviour, Sync-per-unit, and the failure model are unchanged.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `Client#prepare(..., typed: true)` — text format. The first `TUPLES_OK`
|
|
11
|
+
result builds a `TypeMapByColumn`; later dispatches reuse that map from the
|
|
12
|
+
`Request` (resolved at submit, never re-read from handle state at drain).
|
|
13
|
+
Column values arrive as typed Ruby objects (Integer, Time, BigDecimal, …)
|
|
14
|
+
instead of strings, for every PostgreSQL type ruby-pg covers in text mode.
|
|
15
|
+
|
|
16
|
+
### Notes
|
|
17
|
+
|
|
18
|
+
- This is an API feature, not a throughput lever. Typed columns that become
|
|
19
|
+
immediates (`int`, `bool`) drop one heap object each; the rest of the
|
|
20
|
+
per-query allocation is the control plane (`Request`, fibers, `PG::Result`).
|
|
21
|
+
- `CoderMapsBundle` is built on a throwaway connection at the first
|
|
22
|
+
`prepare(..., typed: true)`, never at `Client.start` and never on a live
|
|
23
|
+
pipeline FIFO (a catalog query there desyncs Sync). Untyped `query` /
|
|
24
|
+
`prepare` do not touch type maps on the drain path.
|
|
25
|
+
- A failed type-map apply is a request-local `QueryError`. `typed: true`
|
|
26
|
+
promised a typed value; the handle does not silently fall back to strings.
|
|
27
|
+
- `bigdecimal` is an optional dependency of ruby-pg for `numeric` text
|
|
28
|
+
decoding (required as a gem on Ruby 3.4+). Without it, other typed columns
|
|
29
|
+
still work; `numeric` follows the registry.
|
|
30
|
+
- Default path (no `typed:`): `TypeMapAllStrings`, text format, string keys.
|
|
31
|
+
- Binary result format is not part of this release.
|
|
32
|
+
|
|
33
|
+
## [0.3.2] - 2026-08-17
|
|
34
|
+
|
|
35
|
+
Runtime correctness. Public SQL, Sync-per-unit, failure model, and the
|
|
36
|
+
`Fiber::Scheduler` host contract are unchanged.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Bounded waits (`Notification#wait`, `Task#wait`) park through
|
|
41
|
+
`scheduler.block(blocker, remaining)` and a monotonic deadline. They no
|
|
42
|
+
longer wrap `park` in `Timeout.timeout`. A wait timeout is not a work
|
|
43
|
+
timeout: `Notification#wait` no longer rescues `TimeoutError`, so an
|
|
44
|
+
enclosing `Runtime.with_timeout` cannot be swallowed by
|
|
45
|
+
`BoundedQueue#wait_on` or `wait_for_pinned_idle`. `wait(0)` polls.
|
|
46
|
+
- `Runtime.with_timeout` (health probes and other work limits) calls
|
|
47
|
+
`scheduler.timeout_after(duration, Deadline, message)` when the hook
|
|
48
|
+
exists. `Deadline < Exception`, so `rescue => e` cannot eat it; the
|
|
49
|
+
method still raises `Runtime::TimeoutError` at its own boundary. A
|
|
50
|
+
scheduler without the hook falls back to stdlib `Timeout` and warns
|
|
51
|
+
once — `Thread#raise` can hit the wrong fiber.
|
|
52
|
+
- `Notification#signal` and `Semaphore#release` skip a waiter that
|
|
53
|
+
`Task#stop` already released, or a fiber that is no longer alive. On a
|
|
54
|
+
deferred `unblock` the previous `shift` + wake handed the signal to the
|
|
55
|
+
stopped waiter and left the live one parked; a dead semaphore waiter
|
|
56
|
+
burned a pinned-pool slot. `park` also raises `Cancel` after a stop-wake
|
|
57
|
+
so the wait is not mistaken for a successful signal.
|
|
58
|
+
|
|
59
|
+
### Unchanged
|
|
60
|
+
|
|
61
|
+
- Per-fiber waiter-hash reuse, `Request` one-shot completion, watcher
|
|
62
|
+
`fiber_interrupt` / poll shutdown, and the host-installed scheduler
|
|
63
|
+
rule.
|
|
64
|
+
|
|
3
65
|
## [0.3.1] - 2026-08-09
|
|
4
66
|
|
|
5
67
|
Reliability and correctness fixes on top of 0.3.0. No public API changes other
|
data/README.md
CHANGED
|
@@ -143,6 +143,27 @@ ensure
|
|
|
143
143
|
end
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
Pass `typed: true` to decode cells through ruby-pg's text type map (`Integer`,
|
|
147
|
+
`Time`, `BigDecimal`, …) instead of strings. The map is built from the first
|
|
148
|
+
result and reused. This does not change the wire format or the Sync-per-unit
|
|
149
|
+
model; it is opt-in convenience, not a faster path.
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
by_id = db.prepare(
|
|
153
|
+
"user_by_id",
|
|
154
|
+
"SELECT id, email FROM users WHERE id = $1",
|
|
155
|
+
[23],
|
|
156
|
+
typed: true
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
result = by_id.query([42])
|
|
160
|
+
begin
|
|
161
|
+
result.first["id"] # => 42 (Integer)
|
|
162
|
+
ensure
|
|
163
|
+
result.clear
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
146
167
|
`Client#prepare` returns only after the statement is ready on every currently
|
|
147
168
|
live pipeline connection. Replacement connections automatically prepare the
|
|
148
169
|
registered catalog before they begin accepting requests. The logical name is
|
data/lib/pg_pipeline/client.rb
CHANGED
|
@@ -26,7 +26,9 @@ module PgPipeline
|
|
|
26
26
|
|
|
27
27
|
def start = ClientOps.start(self)
|
|
28
28
|
def query(sql, params = RequestOps::EMPTY_PARAMS) = ClientOps.query(self, sql, params)
|
|
29
|
-
def prepare(name, sql, param_types = nil
|
|
29
|
+
def prepare(name, sql, param_types = nil, typed: false)
|
|
30
|
+
ClientOps.prepare(self, name, sql, param_types, typed: typed)
|
|
31
|
+
end
|
|
30
32
|
def stats = ClientOps.stats(self)
|
|
31
33
|
|
|
32
34
|
def session(&block)
|
|
@@ -78,18 +80,22 @@ module PgPipeline
|
|
|
78
80
|
end
|
|
79
81
|
end
|
|
80
82
|
|
|
81
|
-
def prepare(client, name, sql, param_types)
|
|
83
|
+
def prepare(client, name, sql, param_types, typed: false)
|
|
82
84
|
ensure_started!(client)
|
|
83
85
|
sql = RequestOps.snapshot_sql(sql)
|
|
84
86
|
SessionGuard.assert_multiplexable_normalized!(sql, mode: client.guard)
|
|
85
|
-
pool(client).__send__(:prepare_statement, client, name, sql, param_types)
|
|
87
|
+
pool(client).__send__(:prepare_statement, client, name, sql, param_types, typed: typed)
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
def query_prepared(client, statement, params)
|
|
89
91
|
ensure_started!(client)
|
|
90
92
|
|
|
91
93
|
wait_for_request do
|
|
92
|
-
submit_with_failover(client)
|
|
94
|
+
submit_with_failover(client) do
|
|
95
|
+
request = Request.prepared_query(statement, params: params)
|
|
96
|
+
pool(client).bind_type_map(request, statement) if statement.typed?
|
|
97
|
+
request
|
|
98
|
+
end
|
|
93
99
|
end
|
|
94
100
|
end
|
|
95
101
|
|
|
@@ -13,7 +13,7 @@ module PgPipeline
|
|
|
13
13
|
DEFAULT_MAX_PENDING = 256
|
|
14
14
|
DEFAULT_MAX_IN_FLIGHT = 64
|
|
15
15
|
|
|
16
|
-
attr_reader :conn, :caps, :max_pending, :max_in_flight
|
|
16
|
+
attr_reader :conn, :caps, :max_pending, :max_in_flight, :type_maps
|
|
17
17
|
attr_accessor :socket, :requests, :events, :reader_rearm, :writer_commands,
|
|
18
18
|
:inflight, :dispatching, :submitting, :accepting, :running,
|
|
19
19
|
:draining, :needs_flush, :writer_armed, :request_event_pending,
|
|
@@ -30,11 +30,13 @@ module PgPipeline
|
|
|
30
30
|
def dispatches = @dispatches || 0
|
|
31
31
|
def leaked_watchers = @leaked_watchers || 0
|
|
32
32
|
|
|
33
|
-
def initialize(conn, max_pending: DEFAULT_MAX_PENDING, max_in_flight: DEFAULT_MAX_IN_FLIGHT
|
|
33
|
+
def initialize(conn, max_pending: DEFAULT_MAX_PENDING, max_in_flight: DEFAULT_MAX_IN_FLIGHT,
|
|
34
|
+
type_maps: nil)
|
|
34
35
|
@max_pending = DriverOps.positive_integer!(max_pending, :max_pending)
|
|
35
36
|
@max_in_flight = DriverOps.positive_integer!(max_in_flight, :max_in_flight)
|
|
36
37
|
|
|
37
38
|
@conn = conn
|
|
39
|
+
@type_maps = type_maps
|
|
38
40
|
@caps = ServerCaps.from_connection(conn)
|
|
39
41
|
@caps.assert_supported!
|
|
40
42
|
DriverOps.warn_flush_coupling_once(@caps)
|
|
@@ -387,45 +389,61 @@ module PgPipeline
|
|
|
387
389
|
request = d.inflight.first
|
|
388
390
|
raise ProtocolError, "result without an in-flight request" unless request
|
|
389
391
|
|
|
390
|
-
|
|
391
|
-
request.query_boundary!
|
|
392
|
-
next
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
status = result.result_status
|
|
396
|
-
case status
|
|
397
|
-
when PG::PGRES_TUPLES_OK
|
|
398
|
-
ensure_before_query_boundary!(request, status)
|
|
399
|
-
request.accept_result(result)
|
|
400
|
-
when PG::PGRES_PIPELINE_SYNC
|
|
401
|
-
clear_result(result)
|
|
402
|
-
complete_front(d, request)
|
|
403
|
-
when PG::PGRES_COMMAND_OK, PG::PGRES_EMPTY_QUERY
|
|
404
|
-
ensure_before_query_boundary!(request, status)
|
|
405
|
-
request.accept_result(result)
|
|
406
|
-
when PG::PGRES_FATAL_ERROR
|
|
407
|
-
ensure_before_query_boundary!(request, status)
|
|
408
|
-
request.record_error!(query_error(result), result: result)
|
|
409
|
-
when PG::PGRES_PIPELINE_ABORTED
|
|
410
|
-
ensure_before_query_boundary!(request, status)
|
|
411
|
-
clear_result(result)
|
|
412
|
-
request.record_error!(PipelineAbortedError.new("pipeline unit aborted"))
|
|
413
|
-
when PG::PGRES_BAD_RESPONSE
|
|
414
|
-
clear_result(result)
|
|
415
|
-
raise ProtocolError, "server response was not understood"
|
|
416
|
-
when PG::PGRES_COPY_IN, PG::PGRES_COPY_OUT, PG::PGRES_COPY_BOTH
|
|
417
|
-
clear_result(result)
|
|
418
|
-
raise ProtocolError, "COPY is not supported on the multiplexed pipeline"
|
|
419
|
-
else
|
|
420
|
-
clear_result(result)
|
|
421
|
-
raise ProtocolError, "unexpected pipeline result status #{status}"
|
|
422
|
-
end
|
|
392
|
+
handle_pipeline_result(d, request, result)
|
|
423
393
|
end
|
|
424
394
|
ensure
|
|
425
395
|
d.results_read += read if read.positive?
|
|
426
396
|
end
|
|
427
397
|
end
|
|
428
398
|
|
|
399
|
+
def handle_pipeline_result(d, request, result)
|
|
400
|
+
if result.nil?
|
|
401
|
+
request.query_boundary!
|
|
402
|
+
return
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
status = result.result_status
|
|
406
|
+
case status
|
|
407
|
+
when PG::PGRES_TUPLES_OK
|
|
408
|
+
accept_tuples_ok(d, request, result)
|
|
409
|
+
when PG::PGRES_PIPELINE_SYNC
|
|
410
|
+
clear_result(result)
|
|
411
|
+
complete_front(d, request)
|
|
412
|
+
when PG::PGRES_COMMAND_OK, PG::PGRES_EMPTY_QUERY
|
|
413
|
+
ensure_before_query_boundary!(request, status)
|
|
414
|
+
request.accept_result(result)
|
|
415
|
+
when PG::PGRES_FATAL_ERROR
|
|
416
|
+
ensure_before_query_boundary!(request, status)
|
|
417
|
+
request.record_error!(query_error(result), result: result)
|
|
418
|
+
when PG::PGRES_PIPELINE_ABORTED
|
|
419
|
+
ensure_before_query_boundary!(request, status)
|
|
420
|
+
clear_result(result)
|
|
421
|
+
request.record_error!(PipelineAbortedError.new("pipeline unit aborted"))
|
|
422
|
+
when PG::PGRES_BAD_RESPONSE
|
|
423
|
+
reject_pipeline_status!(result, "server response was not understood")
|
|
424
|
+
when PG::PGRES_COPY_IN, PG::PGRES_COPY_OUT, PG::PGRES_COPY_BOTH
|
|
425
|
+
reject_pipeline_status!(result, "COPY is not supported on the multiplexed pipeline")
|
|
426
|
+
else
|
|
427
|
+
reject_pipeline_status!(result, "unexpected pipeline result status #{status}")
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def accept_tuples_ok(d, request, result)
|
|
432
|
+
ensure_before_query_boundary!(request, PG::PGRES_TUPLES_OK)
|
|
433
|
+
begin
|
|
434
|
+
apply_type_map(d, request, result)
|
|
435
|
+
request.accept_result(result)
|
|
436
|
+
rescue StandardError => e
|
|
437
|
+
message = e.is_a?(Error) ? e.message : "typed result mapping failed: #{e.class}: #{e.message}"
|
|
438
|
+
request.record_error!(QueryError.new(message, cause_result: result), result: result)
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def reject_pipeline_status!(result, message)
|
|
443
|
+
clear_result(result)
|
|
444
|
+
raise ProtocolError, message
|
|
445
|
+
end
|
|
446
|
+
|
|
429
447
|
def ensure_before_query_boundary!(request, status)
|
|
430
448
|
return unless request.query_boundary_seen?
|
|
431
449
|
|
|
@@ -644,6 +662,15 @@ module PgPipeline
|
|
|
644
662
|
nil
|
|
645
663
|
end
|
|
646
664
|
|
|
665
|
+
def apply_type_map(d, request, result)
|
|
666
|
+
return unless request.type_map
|
|
667
|
+
|
|
668
|
+
maps = d.type_maps
|
|
669
|
+
raise Error, "typed statement has no type-map registry" unless maps
|
|
670
|
+
|
|
671
|
+
maps.apply!(request, result, d.conn)
|
|
672
|
+
end
|
|
673
|
+
|
|
647
674
|
def clear_result(result)
|
|
648
675
|
result.clear if result.respond_to?(:clear)
|
|
649
676
|
end
|
data/lib/pg_pipeline/pool.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "errors"
|
|
|
6
6
|
require_relative "runtime"
|
|
7
7
|
require_relative "connection_driver"
|
|
8
8
|
require_relative "prepared_statement"
|
|
9
|
+
require_relative "type_maps"
|
|
9
10
|
require_relative "server_caps"
|
|
10
11
|
|
|
11
12
|
module PgPipeline
|
|
@@ -64,6 +65,7 @@ module PgPipeline
|
|
|
64
65
|
@prepared_statements = {}
|
|
65
66
|
@prepared_generation = 0
|
|
66
67
|
@statement_sequence = 0
|
|
68
|
+
@type_maps = TypeMaps.new
|
|
67
69
|
|
|
68
70
|
@pinned_free = []
|
|
69
71
|
@pinned_in_use = {}
|
|
@@ -113,59 +115,90 @@ module PgPipeline
|
|
|
113
115
|
driver
|
|
114
116
|
end
|
|
115
117
|
|
|
116
|
-
def
|
|
118
|
+
def bind_type_map(request, statement)
|
|
119
|
+
return unless statement.typed?
|
|
120
|
+
|
|
121
|
+
maps = @type_maps
|
|
122
|
+
return unless maps
|
|
123
|
+
|
|
124
|
+
maps.bind(request, statement)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def prepare_statement(client, name, sql, param_types, typed: false)
|
|
117
128
|
ensure_available!
|
|
118
129
|
logical_name = PreparedStatementOps.snapshot_name(name)
|
|
119
130
|
if @prepared_statements.key?(logical_name)
|
|
120
131
|
raise Error, "prepared statement #{logical_name.inspect} already exists"
|
|
121
132
|
end
|
|
122
133
|
|
|
134
|
+
statement = register_prepared_statement(client, logical_name, sql, param_types, typed)
|
|
135
|
+
requests = []
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
requests = dispatch_prepare(statement)
|
|
139
|
+
await_prepares!(requests)
|
|
140
|
+
statement
|
|
141
|
+
rescue Exception
|
|
142
|
+
drop_prepared_statement(logical_name, statement)
|
|
143
|
+
raise
|
|
144
|
+
ensure
|
|
145
|
+
requests.each { |request| request.cancel! unless request.settled? }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def register_prepared_statement(client, logical_name, sql, param_types, typed)
|
|
123
150
|
@statement_sequence += 1
|
|
124
151
|
statement = PreparedStatement.new(
|
|
125
152
|
client: client,
|
|
126
153
|
name: logical_name,
|
|
127
154
|
physical_name: "pgp_#{@statement_sequence.to_s(36)}",
|
|
128
155
|
sql: sql,
|
|
129
|
-
param_types: param_types
|
|
156
|
+
param_types: param_types,
|
|
157
|
+
typed: typed
|
|
130
158
|
)
|
|
131
159
|
|
|
132
160
|
@prepared_statements[logical_name] = statement
|
|
161
|
+
register_typed_maps(statement) if statement.typed?
|
|
133
162
|
@prepared_generation += 1
|
|
134
|
-
|
|
163
|
+
statement
|
|
164
|
+
end
|
|
135
165
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
def register_typed_maps(statement)
|
|
167
|
+
@type_maps ||= TypeMaps.new
|
|
168
|
+
@type_maps.register(statement)
|
|
169
|
+
@type_maps.ensure_bundle!(@connection_args)
|
|
170
|
+
end
|
|
141
171
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
def dispatch_prepare(statement)
|
|
173
|
+
drivers = @drivers.select(&:available?)
|
|
174
|
+
if drivers.empty?
|
|
175
|
+
raise NotDispatchedError, "no live pipeline connections; statement was not prepared"
|
|
176
|
+
end
|
|
145
177
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
begin
|
|
149
|
-
RequestOps.clear_result(request.wait)
|
|
150
|
-
rescue StandardError => e
|
|
151
|
-
if first_error
|
|
152
|
-
e.clear_result! if e.respond_to?(:clear_result!)
|
|
153
|
-
else
|
|
154
|
-
first_error = e
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
raise first_error if first_error
|
|
178
|
+
drivers.map { |driver| Request.prepare(statement).tap { |r| driver.submit(r) } }
|
|
179
|
+
end
|
|
159
180
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
181
|
+
def await_prepares!(requests)
|
|
182
|
+
first_error = nil
|
|
183
|
+
requests.each do |request|
|
|
184
|
+
begin
|
|
185
|
+
RequestOps.clear_result(request.wait)
|
|
186
|
+
rescue StandardError => e
|
|
187
|
+
if first_error
|
|
188
|
+
e.clear_result! if e.respond_to?(:clear_result!)
|
|
189
|
+
else
|
|
190
|
+
first_error = e
|
|
191
|
+
end
|
|
164
192
|
end
|
|
165
|
-
raise
|
|
166
|
-
ensure
|
|
167
|
-
requests.each { |request| request.cancel! unless request.settled? }
|
|
168
193
|
end
|
|
194
|
+
raise first_error if first_error
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def drop_prepared_statement(logical_name, statement)
|
|
198
|
+
return unless @prepared_statements.delete(logical_name)
|
|
199
|
+
|
|
200
|
+
@type_maps.unregister(statement.physical_name) if statement&.typed? && @type_maps
|
|
201
|
+
@prepared_generation += 1
|
|
169
202
|
end
|
|
170
203
|
|
|
171
204
|
def with_pinned
|
|
@@ -174,31 +207,11 @@ module PgPipeline
|
|
|
174
207
|
raise Error, "pinned pool is disabled (pinned_size=0)" if @pinned_size.zero?
|
|
175
208
|
|
|
176
209
|
owner = Fiber.current
|
|
177
|
-
|
|
178
|
-
raise RecursiveCheckoutError,
|
|
179
|
-
"nested Client#session/transaction on the same fiber is not allowed; " \
|
|
180
|
-
"use Transaction#savepoint for nested atomicity"
|
|
181
|
-
end
|
|
210
|
+
assert_not_nested_pinned!(owner)
|
|
182
211
|
|
|
183
212
|
@pinned_gate.acquire do
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
conn = nil
|
|
188
|
-
@pinned_active += 1
|
|
189
|
-
@pinned_owners[owner] += 1
|
|
190
|
-
|
|
191
|
-
begin
|
|
192
|
-
conn = @pinned_free.pop || PoolOps.new_connection(@connection_args)
|
|
193
|
-
raise @pinned_error if @pinned_error
|
|
194
|
-
raise ShutdownError, "pool is closing" if @closing
|
|
195
|
-
|
|
196
|
-
@pinned_in_use[conn] = owner
|
|
197
|
-
yield conn
|
|
198
|
-
ensure
|
|
199
|
-
@pinned_in_use.delete(conn) if conn
|
|
200
|
-
release_pinned(owner, conn)
|
|
201
|
-
end
|
|
213
|
+
assert_pinned_open!
|
|
214
|
+
run_pinned(owner) { |conn| yield conn }
|
|
202
215
|
end
|
|
203
216
|
end
|
|
204
217
|
|
|
@@ -453,7 +466,7 @@ module PgPipeline
|
|
|
453
466
|
def start_pipeline_driver
|
|
454
467
|
conn = PoolOps.new_connection(@connection_args)
|
|
455
468
|
prepare_registered_statements(conn)
|
|
456
|
-
driver = ConnectionDriver.new(conn, max_pending: @max_pending, max_in_flight: @max_in_flight)
|
|
469
|
+
driver = ConnectionDriver.new(conn, max_pending: @max_pending, max_in_flight: @max_in_flight, type_maps: @type_maps)
|
|
457
470
|
driver.start
|
|
458
471
|
rescue Exception
|
|
459
472
|
PoolOps.safe_close(conn) if conn
|
|
@@ -471,10 +484,11 @@ module PgPipeline
|
|
|
471
484
|
next if prepared.key?(statement.physical_name)
|
|
472
485
|
|
|
473
486
|
result = if statement.param_types.nil?
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
487
|
+
conn.prepare(statement.physical_name, statement.sql)
|
|
488
|
+
else
|
|
489
|
+
conn.prepare(statement.physical_name, statement.sql, statement.param_types)
|
|
490
|
+
end
|
|
491
|
+
|
|
478
492
|
RequestOps.clear_result(result)
|
|
479
493
|
prepared[statement.physical_name] = true
|
|
480
494
|
end
|
|
@@ -483,22 +497,41 @@ module PgPipeline
|
|
|
483
497
|
end
|
|
484
498
|
end
|
|
485
499
|
|
|
486
|
-
def
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
500
|
+
def assert_not_nested_pinned!(owner)
|
|
501
|
+
return unless @pinned_owners[owner].positive?
|
|
502
|
+
|
|
503
|
+
raise RecursiveCheckoutError,
|
|
504
|
+
"nested Client#session/transaction on the same fiber is not allowed; " \
|
|
505
|
+
"use Transaction#savepoint for nested atomicity"
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def assert_pinned_open!
|
|
509
|
+
raise @pinned_error if @pinned_error
|
|
510
|
+
raise ShutdownError, "pool is closing" if @closing
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def run_pinned(owner)
|
|
514
|
+
conn = nil
|
|
515
|
+
@pinned_active += 1
|
|
516
|
+
@pinned_owners[owner] += 1
|
|
517
|
+
|
|
518
|
+
begin
|
|
519
|
+
conn = take_pinned_connection
|
|
520
|
+
assert_pinned_open!
|
|
521
|
+
@pinned_in_use[conn] = owner
|
|
522
|
+
yield conn
|
|
523
|
+
ensure
|
|
524
|
+
@pinned_in_use.delete(conn) if conn
|
|
525
|
+
release_pinned(owner, conn)
|
|
501
526
|
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def take_pinned_connection
|
|
530
|
+
@pinned_free.pop || PoolOps.new_connection(@connection_args)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def release_pinned(owner, conn)
|
|
534
|
+
return_pinned_connection(conn) if conn
|
|
502
535
|
ensure
|
|
503
536
|
@pinned_active -= 1
|
|
504
537
|
@pinned_owners[owner] -= 1
|
|
@@ -506,6 +539,22 @@ module PgPipeline
|
|
|
506
539
|
@pinned_idle.signal if @pinned_active.zero?
|
|
507
540
|
end
|
|
508
541
|
|
|
542
|
+
def return_pinned_connection(conn)
|
|
543
|
+
if @closing
|
|
544
|
+
PoolOps.safe_close(conn)
|
|
545
|
+
return
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
recycled = recycle_pinned_connection(conn)
|
|
549
|
+
return unless recycled
|
|
550
|
+
|
|
551
|
+
if @closing
|
|
552
|
+
PoolOps.safe_close(recycled)
|
|
553
|
+
else
|
|
554
|
+
@pinned_free << recycled
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
509
558
|
def recycle_pinned_connection(conn)
|
|
510
559
|
if conn.server_version < DISCARD_SEQUENCES_SERVER_VERSION
|
|
511
560
|
PoolOps.safe_close(conn)
|
|
@@ -602,9 +651,7 @@ module PgPipeline
|
|
|
602
651
|
|
|
603
652
|
start = rr % size
|
|
604
653
|
best = nil
|
|
605
|
-
best_index = 0
|
|
606
|
-
best_load = 0
|
|
607
|
-
offset = 0
|
|
654
|
+
best_index, best_load, offset = 0, 0, 0
|
|
608
655
|
|
|
609
656
|
while offset < size
|
|
610
657
|
index = start + offset
|
|
@@ -5,22 +5,26 @@ require_relative "request"
|
|
|
5
5
|
|
|
6
6
|
module PgPipeline
|
|
7
7
|
class PreparedStatement
|
|
8
|
-
attr_reader :name, :sql, :param_types, :physical_name
|
|
8
|
+
attr_reader :name, :sql, :param_types, :physical_name, :typed
|
|
9
9
|
|
|
10
|
-
def initialize(client:, name:, physical_name:, sql:, param_types: nil)
|
|
10
|
+
def initialize(client:, name:, physical_name:, sql:, param_types: nil, typed: false)
|
|
11
11
|
@client = client
|
|
12
12
|
@name = PreparedStatementOps.snapshot_name(name)
|
|
13
13
|
@physical_name = PreparedStatementOps.snapshot_name(physical_name)
|
|
14
14
|
@sql = RequestOps.snapshot_sql(sql)
|
|
15
15
|
@param_types = RequestOps.snapshot_param_types(param_types)
|
|
16
|
+
@typed = !!typed
|
|
16
17
|
freeze
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
def typed? = @typed
|
|
21
|
+
|
|
19
22
|
def query(params = RequestOps::EMPTY_PARAMS) = PreparedStatementOps.query(self, params)
|
|
20
23
|
alias call query
|
|
21
24
|
|
|
22
25
|
def inspect
|
|
23
|
-
|
|
26
|
+
flag = @typed ? " typed" : ""
|
|
27
|
+
"#<#{self.class} name=#{@name.inspect} sql=#{@sql.inspect}#{flag}>"
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
private
|
|
@@ -42,6 +46,5 @@ module PgPipeline
|
|
|
42
46
|
|
|
43
47
|
value.frozen? ? value : value.dup.freeze
|
|
44
48
|
end
|
|
45
|
-
|
|
46
49
|
end
|
|
47
50
|
end
|
data/lib/pg_pipeline/request.rb
CHANGED
|
@@ -6,7 +6,7 @@ module PgPipeline
|
|
|
6
6
|
class Request
|
|
7
7
|
attr_reader :sql, :params
|
|
8
8
|
attr_accessor :state, :cancelled, :settled, :result_seen, :query_boundary_seen,
|
|
9
|
-
:result, :error, :waiter, :waiter_scheduler
|
|
9
|
+
:result, :error, :waiter, :waiter_scheduler, :type_map
|
|
10
10
|
|
|
11
11
|
def initialize(sql:, params: nil)
|
|
12
12
|
@sql = RequestOps.snapshot_sql(sql)
|
|
@@ -20,6 +20,7 @@ module PgPipeline
|
|
|
20
20
|
@error = nil
|
|
21
21
|
@waiter = nil
|
|
22
22
|
@waiter_scheduler = nil
|
|
23
|
+
@type_map = nil
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def self.build(sql, params)
|
|
@@ -64,6 +65,7 @@ module PgPipeline
|
|
|
64
65
|
@error = nil
|
|
65
66
|
@waiter = nil
|
|
66
67
|
@waiter_scheduler = nil
|
|
68
|
+
@type_map = nil
|
|
67
69
|
self
|
|
68
70
|
end
|
|
69
71
|
end
|
|
@@ -7,19 +7,24 @@ module PgPipeline
|
|
|
7
7
|
@waiters = []
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
def waiting = @waiters.size
|
|
11
|
+
|
|
10
12
|
def wait(timeout = nil)
|
|
13
|
+
wait_until(Runtime.deadline_for(timeout))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def wait_until(deadline = nil)
|
|
11
17
|
Runtime.with_waiter(self, @waiters) do |waiter|
|
|
12
|
-
Runtime.
|
|
13
|
-
true
|
|
14
|
-
rescue TimeoutError
|
|
15
|
-
false
|
|
18
|
+
Runtime.park(self, waiter, deadline) { false }
|
|
16
19
|
end
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
def signal
|
|
20
|
-
waiter = @waiters.shift
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
while (waiter = @waiters.shift)
|
|
24
|
+
return true if Runtime.wake_dequeued(waiter, self)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
false
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
def signal_all
|
|
@@ -23,12 +23,12 @@ module PgPipeline
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def release
|
|
26
|
-
|
|
26
|
+
while (node = @waiting.shift)
|
|
27
27
|
node.grant!
|
|
28
|
-
node.resume
|
|
29
|
-
else
|
|
30
|
-
@available += 1
|
|
28
|
+
return @available if node.resume
|
|
31
29
|
end
|
|
30
|
+
|
|
31
|
+
@available += 1
|
|
32
32
|
@available
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -126,9 +126,10 @@ module PgPipeline
|
|
|
126
126
|
|
|
127
127
|
def resume
|
|
128
128
|
fiber = @fiber
|
|
129
|
-
return unless fiber.alive?
|
|
129
|
+
return false unless fiber.alive?
|
|
130
130
|
|
|
131
131
|
@scheduler.unblock(@blocker, fiber)
|
|
132
|
+
true
|
|
132
133
|
end
|
|
133
134
|
end
|
|
134
135
|
|
|
@@ -22,7 +22,10 @@ module PgPipeline
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def wait(timeout = nil)
|
|
25
|
-
|
|
25
|
+
unless @done
|
|
26
|
+
deadline = Runtime.deadline_for(timeout)
|
|
27
|
+
raise TimeoutError, "task did not finish in time" unless join(deadline)
|
|
28
|
+
end
|
|
26
29
|
raise @error if @error
|
|
27
30
|
|
|
28
31
|
@result
|
|
@@ -94,8 +97,8 @@ module PgPipeline
|
|
|
94
97
|
|
|
95
98
|
def release_blocker
|
|
96
99
|
waiter = @blocker or return false
|
|
97
|
-
|
|
98
|
-
waiter[:fiber].alive?
|
|
100
|
+
|
|
101
|
+
Runtime.wake(waiter, waiter[:blocker]) && waiter[:fiber].alive?
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
def complete(result, error)
|
|
@@ -107,11 +110,11 @@ module PgPipeline
|
|
|
107
110
|
wake_waiters
|
|
108
111
|
end
|
|
109
112
|
|
|
110
|
-
def join
|
|
111
|
-
return if @done
|
|
113
|
+
def join(deadline)
|
|
114
|
+
return true if @done
|
|
112
115
|
|
|
113
116
|
Runtime.with_waiter(self, @waiters) do |waiter|
|
|
114
|
-
Runtime.park(self, waiter) { @done }
|
|
117
|
+
Runtime.park(self, waiter, deadline) { @done }
|
|
115
118
|
end
|
|
116
119
|
end
|
|
117
120
|
|
data/lib/pg_pipeline/runtime.rb
CHANGED
|
@@ -7,12 +7,25 @@ require_relative "errors"
|
|
|
7
7
|
module PgPipeline
|
|
8
8
|
module Runtime
|
|
9
9
|
class Cancel < Exception; end
|
|
10
|
+
class Deadline < Exception; end
|
|
10
11
|
class TimeoutError < Error; end
|
|
11
12
|
|
|
12
13
|
module_function
|
|
13
14
|
|
|
14
15
|
CURRENT_TASK_KEY = :pg_pipeline_current_task
|
|
15
16
|
WAITER_KEY = :pg_pipeline_waiter
|
|
17
|
+
DEADLINE_MESSAGE = "execution expired"
|
|
18
|
+
|
|
19
|
+
MISSING_TIMEOUT_HOOK_WARNING = <<~MESSAGE
|
|
20
|
+
pg_pipeline: %s does not implement #timeout_after.
|
|
21
|
+
|
|
22
|
+
Falling back to stdlib Timeout, which uses Thread#raise and can deliver
|
|
23
|
+
the timeout to an unrelated fiber. Use a scheduler that implements
|
|
24
|
+
#timeout_after (async, itsi-scheduler) or avoid Runtime.with_timeout
|
|
25
|
+
on this host.
|
|
26
|
+
MESSAGE
|
|
27
|
+
|
|
28
|
+
@warned_schedulers = {}
|
|
16
29
|
|
|
17
30
|
def spawn(name: nil, &block)
|
|
18
31
|
Task.spawn(name: name, &block)
|
|
@@ -22,6 +35,10 @@ module PgPipeline
|
|
|
22
35
|
Fiber.scheduler or raise Error, "operation requires an active Fiber scheduler"
|
|
23
36
|
end
|
|
24
37
|
|
|
38
|
+
def native_timeouts?(target = Fiber.scheduler)
|
|
39
|
+
!target.nil? && target.respond_to?(:timeout_after)
|
|
40
|
+
end
|
|
41
|
+
|
|
25
42
|
def current_task
|
|
26
43
|
Thread.current[CURRENT_TASK_KEY]
|
|
27
44
|
end
|
|
@@ -44,16 +61,20 @@ module PgPipeline
|
|
|
44
61
|
waiter
|
|
45
62
|
end
|
|
46
63
|
|
|
47
|
-
def park(blocker, waiter)
|
|
64
|
+
def park(blocker, waiter, deadline = nil)
|
|
48
65
|
scheduler = waiter[:scheduler]
|
|
49
66
|
task = current_task
|
|
50
67
|
task&.enter_block(waiter)
|
|
51
68
|
|
|
52
69
|
until waiter[:ready] || yield
|
|
53
|
-
|
|
70
|
+
remaining = deadline ? (deadline - monotonic_now) : nil
|
|
71
|
+
return false if deadline && remaining <= 0
|
|
72
|
+
|
|
73
|
+
scheduler.block(blocker, remaining)
|
|
54
74
|
task&.raise_if_cancelled!
|
|
55
75
|
end
|
|
56
|
-
|
|
76
|
+
task&.raise_if_cancelled!
|
|
77
|
+
true
|
|
57
78
|
ensure
|
|
58
79
|
waiter[:blocker] = nil
|
|
59
80
|
task&.exit_block
|
|
@@ -65,7 +86,10 @@ module PgPipeline
|
|
|
65
86
|
waiters << waiter
|
|
66
87
|
yield waiter
|
|
67
88
|
ensure
|
|
68
|
-
|
|
89
|
+
if waiter[:queued]
|
|
90
|
+
waiters.delete(waiter)
|
|
91
|
+
waiter[:queued] = false
|
|
92
|
+
end
|
|
69
93
|
end
|
|
70
94
|
|
|
71
95
|
def wake_dequeued(waiter, blocker)
|
|
@@ -73,21 +97,59 @@ module PgPipeline
|
|
|
73
97
|
wake(waiter, blocker)
|
|
74
98
|
end
|
|
75
99
|
|
|
100
|
+
def wake(waiter, blocker)
|
|
101
|
+
return false if waiter[:ready]
|
|
102
|
+
|
|
103
|
+
waiter[:ready] = true
|
|
104
|
+
fiber = waiter[:fiber]
|
|
105
|
+
return false unless fiber.alive?
|
|
106
|
+
|
|
107
|
+
waiter[:scheduler].unblock(blocker, fiber)
|
|
108
|
+
true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def monotonic_now
|
|
112
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def deadline_for(timeout)
|
|
116
|
+
return nil if timeout.nil?
|
|
117
|
+
|
|
118
|
+
seconds = Float(timeout)
|
|
119
|
+
raise ArgumentError, "timeout must be non-negative and finite" unless seconds.finite? && seconds >= 0
|
|
120
|
+
|
|
121
|
+
monotonic_now + seconds
|
|
122
|
+
end
|
|
123
|
+
|
|
76
124
|
def with_timeout(duration)
|
|
77
125
|
return yield if duration.nil?
|
|
78
126
|
|
|
79
|
-
|
|
80
|
-
raise ArgumentError, "timeout must be non-negative and finite" unless
|
|
127
|
+
seconds = Float(duration)
|
|
128
|
+
raise ArgumentError, "timeout must be non-negative and finite" unless seconds.finite? && seconds >= 0
|
|
81
129
|
|
|
82
|
-
|
|
130
|
+
begin
|
|
131
|
+
arm_deadline(seconds) { yield }
|
|
132
|
+
rescue Deadline => e
|
|
133
|
+
raise TimeoutError, e.message
|
|
134
|
+
end
|
|
83
135
|
end
|
|
84
136
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
137
|
+
def arm_deadline(seconds, &block)
|
|
138
|
+
target = Fiber.scheduler
|
|
139
|
+
if native_timeouts?(target)
|
|
140
|
+
return target.timeout_after(seconds, Deadline, DEADLINE_MESSAGE, &block)
|
|
141
|
+
end
|
|
89
142
|
|
|
90
|
-
|
|
143
|
+
warn_missing_timeout_hook(target) if target
|
|
144
|
+
::Timeout.timeout(seconds, Deadline, DEADLINE_MESSAGE, &block)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def warn_missing_timeout_hook(target)
|
|
148
|
+
key = target.class
|
|
149
|
+
return if @warned_schedulers[key]
|
|
150
|
+
|
|
151
|
+
@warned_schedulers[key] = true
|
|
152
|
+
warn(format(MISSING_TIMEOUT_HOOK_WARNING, key))
|
|
91
153
|
end
|
|
92
154
|
end
|
|
93
155
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pg"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module PgPipeline
|
|
7
|
+
class TypeMaps
|
|
8
|
+
PENDING = Object.new.freeze
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@cache = {}
|
|
12
|
+
@wants_typed = {}
|
|
13
|
+
@bundle = nil
|
|
14
|
+
@text_map_for_results = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def register(statement)
|
|
18
|
+
@wants_typed[statement.physical_name] = true if statement.typed?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def unregister(physical_name)
|
|
22
|
+
@cache.delete(physical_name)
|
|
23
|
+
@wants_typed.delete(physical_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def typed?(physical_name)
|
|
27
|
+
@wants_typed[physical_name]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def cached(physical_name)
|
|
31
|
+
@cache[physical_name]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def bind(request, statement)
|
|
35
|
+
return unless statement.typed?
|
|
36
|
+
|
|
37
|
+
request.type_map = @cache[statement.physical_name] || PENDING
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ensure_bundle!(connection_args)
|
|
41
|
+
return if @bundle && @bundle != :failed
|
|
42
|
+
|
|
43
|
+
conn = nil
|
|
44
|
+
conn = connection_args.nil? ? PG::Connection.new : PG::Connection.new(connection_args)
|
|
45
|
+
@bundle = PG::BasicTypeRegistry::CoderMapsBundle.new(conn)
|
|
46
|
+
@text_map_for_results = PG::BasicTypeMapForResults.new(@bundle)
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
@bundle = :failed
|
|
49
|
+
@text_map_for_results = nil
|
|
50
|
+
raise Error, "failed to build CoderMapsBundle (#{e.class}: #{e.message})"
|
|
51
|
+
ensure
|
|
52
|
+
begin
|
|
53
|
+
conn&.close
|
|
54
|
+
rescue StandardError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def apply!(request, result, _conn)
|
|
60
|
+
map = request.type_map
|
|
61
|
+
return if map.nil?
|
|
62
|
+
|
|
63
|
+
if map.equal?(PENDING)
|
|
64
|
+
name = request.statement_name
|
|
65
|
+
map = @cache[name]
|
|
66
|
+
unless map
|
|
67
|
+
map = build_column_map!(result)
|
|
68
|
+
@cache[name] = map
|
|
69
|
+
end
|
|
70
|
+
request.type_map = map
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
result.type_map = map
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def build_column_map!(result)
|
|
79
|
+
unless result.result_status == PG::PGRES_TUPLES_OK && result.nfields.positive?
|
|
80
|
+
raise Error, "typed mapping requires a TUPLES_OK result with at least one column"
|
|
81
|
+
end
|
|
82
|
+
if @bundle == :failed || @text_map_for_results.nil?
|
|
83
|
+
raise Error, "typed result mapping is unavailable (CoderMapsBundle failed)"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
@text_map_for_results.build_column_map(result)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/pg_pipeline/version.rb
CHANGED
data/lib/pg_pipeline.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "pg_pipeline/session"
|
|
|
9
9
|
require_relative "pg_pipeline/transaction"
|
|
10
10
|
require_relative "pg_pipeline/request"
|
|
11
11
|
require_relative "pg_pipeline/prepared_statement"
|
|
12
|
+
require_relative "pg_pipeline/type_maps"
|
|
12
13
|
require_relative "pg_pipeline/connection_driver"
|
|
13
14
|
require_relative "pg_pipeline/pool"
|
|
14
15
|
require_relative "pg_pipeline/client"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pg_pipeline
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Hajdarov
|
|
@@ -134,6 +134,7 @@ files:
|
|
|
134
134
|
- lib/pg_pipeline/session.rb
|
|
135
135
|
- lib/pg_pipeline/session_guard.rb
|
|
136
136
|
- lib/pg_pipeline/transaction.rb
|
|
137
|
+
- lib/pg_pipeline/type_maps.rb
|
|
137
138
|
- lib/pg_pipeline/version.rb
|
|
138
139
|
homepage: https://github.com/roman-haidarov/pg_pipeline
|
|
139
140
|
licenses:
|