pipeloader 0.0.2 → 0.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3dfa060eef9aa9ccdeb6bf2408b3289a06daca11cc8b53e3b1ff476964a27577
4
- data.tar.gz: 2c419c78fb32aafbf9f76e7a376afae92d82a9f4e67f2ecb2f31f00f97bbbd43
3
+ metadata.gz: 2efb3ab68daaf2649998d53eab252167254d1e195ff3f46ff676c1bcaeeb8d17
4
+ data.tar.gz: a45b1c6236205caf6421437bdf4ec6b04b459698d630f63708c6f929e63c1242
5
5
  SHA512:
6
- metadata.gz: 81b2c029fc1226ab37b620a9699fddc873d979aa82ff693ed5b4b6539326aed038fd0de4a2431b37f8571a390c0b5638da1e41c4b784341f1f9caeb4aadeeed6
7
- data.tar.gz: f905883e768f604f436d4e64b29bd84fea16bbe6d3bfadf68ec4b423ec0824015cf166ac73ba3c68d9536f0e0a9303b7bb53b09be58f5e9f0bdfbb880b03a767
6
+ metadata.gz: c607f4d9a7c5c4c5ee035bc2458ad3e91b0a1d15e83d7a9cedd35a2942de1c470521175eff0d9898211c828575c1615aeb514212e3edb252159686b5aa11864b
7
+ data.tar.gz: 5d9dbf27e303c91733d6f6b5787015bf993a9658b173f689e4cecbd9a0a55739ac4d691976bc4cfe034f0b5d0d8e541717c1a57a3b6780ed58b57c454d3e7180
@@ -26,7 +26,9 @@ module Pipeloader
26
26
  sql, bind_objs, = to_sql_and_binds(relation, binds, preparable, allow_retry)
27
27
  if sql.lstrip[0, 6].casecmp("select").zero?
28
28
  params = bind_objs.map { |b| b.respond_to?(:value_for_database) ? b.value_for_database : b }
29
- return dl.with(Pipeloader::Source, raw_connection).load([sql, params])
29
+ # Hand the Source this connection (self): it pipelines on raw_connection
30
+ # and types results via the adapter's OID lookup. One Source per checkout.
31
+ return dl.with(Pipeloader::Source, self).load([sql, params])
30
32
  end
31
33
  end
32
34
  # Synchronous fallback (no active response, or not gathered): one query,
@@ -180,7 +180,7 @@ module Pipeloader
180
180
  # key, single), :by_fk_one (has_one, demux by FK, first) or :by_fk_many (has_many,
181
181
  # demux by FK, array). All fused lookups on a connection share ONE FusionSource.
182
182
  def self.fuse(dataloader, model, kind, key, columns, value)
183
- dataloader.with(FusionSource, model.connection.raw_connection)
183
+ dataloader.with(FusionSource, model.connection)
184
184
  .load([kind, model, key, columns, value].freeze)
185
185
  end
186
186
 
@@ -192,15 +192,15 @@ module Pipeloader
192
192
  # WITHOUT forcing (`request`), then force them together — so a whole level's fused
193
193
  # lookups collapse into one pipeline burst and round trips stay = tree depth.
194
194
  class FusionSource < GraphQL::Dataloader::Source
195
- def initialize(pg)
196
- @pg = pg
195
+ def initialize(conn)
196
+ @conn = conn
197
197
  end
198
198
 
199
199
  # descriptors: [kind, model, key, columns, value], deduped by Dataloader (so two
200
200
  # parents sharing a belongs_to target hit the DB once). Returns one demuxed value
201
201
  # per descriptor, in order.
202
202
  def fetch(descriptors)
203
- src = @dataloader.with(Pipeloader::Source, @pg)
203
+ src = @dataloader.with(Pipeloader::Source, @conn)
204
204
 
205
205
  # One `WHERE key = ANY($1)` per distinct query shape, enqueued but not forced.
206
206
  pending = descriptors.group_by { |d| d[0, 4] }.map do |(kind, model, key, columns), ds|
@@ -5,8 +5,9 @@ module Pipeloader
5
5
  module Pipeliner
6
6
  module_function
7
7
 
8
- # queries: array of [sql, params]. Returns array of [columns, rows] (raw
9
- # strings), in the same order, having sent them all in a single round trip.
8
+ # queries: array of [sql, params]. Returns array of [columns, rows, oids]
9
+ # (raw string values + per-column [oid, fmod] so the Source can type them),
10
+ # in the same order, having sent them all in a single round trip.
10
11
  #
11
12
  # Prepared statements are cached for the lifetime of the REQUEST (the cache and
12
13
  # name space are set up by Pipeloader::Trace per multiplex), so a shape is
@@ -71,10 +72,14 @@ module Pipeloader
71
72
  break if status == PG::PGRES_PIPELINE_SYNC
72
73
 
73
74
  if status == PG::PGRES_TUPLES_OK && results
74
- # Raw strings, so ActiveRecord casts via its own column types (and so we
75
- # never disturb the connection's type map that AR relies on).
75
+ # Read values as strings (so we never disturb the connection's type map
76
+ # that AR relies on), and capture each column's result OID + modifier so
77
+ # the Source can resolve the real type via the adapter's get_oid_type —
78
+ # including computed/aliased columns the model has no column type for.
79
+ # ftype/fmod are result metadata, unaffected by the all-strings map.
76
80
  result.type_map = PG::TypeMapAllStrings.new
77
- results << [result.fields, result.values]
81
+ oids = Array.new(result.nfields) { |i| [result.ftype(i), result.fmod(i)] }
82
+ results << [result.fields, result.values, oids]
78
83
  elsif status != PG::PGRES_COMMAND_OK && capture_error
79
84
  begin
80
85
  result.check
@@ -3,8 +3,12 @@ module Pipeloader
3
3
  # and runs them as one pipelined burst, returning an ActiveRecord::Result per
4
4
  # query (so AR builds models normally).
5
5
  class Source < GraphQL::Dataloader::Source
6
- def initialize(pg)
7
- @pg = pg
6
+ # conn: the AR connection threaded in from the call site (the QueryCache
7
+ # patch's own connection, or the model's in FusionSource). @pg drives the
8
+ # pipeline; @conn supplies the OID -> type lookup for building typed results.
9
+ def initialize(conn)
10
+ @conn = conn
11
+ @pg = conn.raw_connection
8
12
  end
9
13
 
10
14
  # keys: array of [sql, params], deduplicated by Dataloader. Must return one
@@ -13,7 +17,27 @@ module Pipeloader
13
17
  batch = Pipeliner.pipeline_batch(@pg, keys)
14
18
  Pipeloader.round_trips += 1
15
19
  Pipeloader.queries += keys.size
16
- batch.map { |columns, rows| ActiveRecord::Result.new(columns, rows) }
20
+ batch.map { |columns, rows, oids| to_ar_result(columns, rows, oids) }
21
+ end
22
+
23
+ private
24
+
25
+ # Build a typed ActiveRecord::Result from the raw-string rows. The model casts
26
+ # its own table columns, but a computed/aliased column (`COUNT(*) AS n`,
27
+ # `view_count > 1 AS hot`) has no model type, so without help it would stay the
28
+ # raw Postgres string — and "f" is truthy in Ruby. Resolve each column's type
29
+ # from its result OID via the adapter's own get_oid_type (exactly what AR does
30
+ # for a normal query), keyed by both name and index so AR's Result finds it on
31
+ # every supported version (7.x looks up column_types by name, 8.x by index).
32
+ def to_ar_result(columns, rows, oids)
33
+ column_types = {}
34
+ columns.each_with_index do |name, i|
35
+ oid, fmod = oids[i]
36
+ type = @conn.send(:get_oid_type, oid, fmod, name)
37
+ column_types[name] = type
38
+ column_types[i] = type
39
+ end
40
+ ActiveRecord::Result.new(columns, rows, column_types)
17
41
  end
18
42
  end
19
43
  end
@@ -1,3 +1,3 @@
1
1
  module Pipeloader
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pipeloader
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Hull