Skip to content

Commit

Permalink
Add CoderMapsBundle for building BasicTypeMap instances
Browse files Browse the repository at this point in the history
This way one CoderMapsBundle can be used to build several BasicTypeMap instances.
So only one database query on pg_types is executed then.

Follow up to #376
  • Loading branch information
larskanis committed Jul 30, 2021
1 parent 7fe1eba commit 4c6c6b2
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 38 deletions.
83 changes: 53 additions & 30 deletions lib/pg/basic_type_mapping.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,25 +87,50 @@ def coder_by_oid(oid)
end
end

def build_coder_maps(connection)
result = connection.exec(<<-SQL).to_a
SELECT t.oid, t.typname, t.typelem, t.typdelim, ti.proname AS typinput
FROM pg_type as t
JOIN pg_proc as ti ON ti.oid = t.typinput
SQL

[
[0, :encoder, PG::TextEncoder::Array],
[0, :decoder, PG::TextDecoder::Array],
[1, :encoder, nil],
[1, :decoder, nil],
].inject([]) do |h, (format, direction, arraycoder)|
h[format] ||= {}
h[format][direction] = CoderMap.new result, CODERS_BY_NAME[format][direction], format, arraycoder
h
# An instance of this class stores CoderMap instances to be used for text and binary wire formats
# as well as encoder and decoder directions.
#
# A PG::BasicTypeRegistry::CoderMapsBundle instance retrieves all type definitions from the PostgreSQL server and matches them with the coder definitions of the global PG::BasicTypeRegistry .
# It provides 4 separate CoderMap instances for the combinations of the two formats and directions.
#
# A PG::BasicTypeRegistry::CoderMapsBundle instance can be used to initialize an instance of
# * PG::BasicTypeMapForResults
# * PG::BasicTypeMapForQueries
# * PG::BasicTypeMapBasedOnResult
# by passing it instead of the connection object like so:
#
# conn = PG::Connection.new
# maps = PG::BasicTypeRegistry::CoderMapsBundle.new(conn)
# conn.type_map_for_results = PG::BasicTypeMapForResults.new(maps)
#
class CoderMapsBundle
def initialize(connection)
result = connection.exec(<<-SQL).to_a
SELECT t.oid, t.typname, t.typelem, t.typdelim, ti.proname AS typinput
FROM pg_type as t
JOIN pg_proc as ti ON ti.oid = t.typinput
SQL

@maps = [
[0, :encoder, PG::TextEncoder::Array],
[0, :decoder, PG::TextDecoder::Array],
[1, :encoder, nil],
[1, :decoder, nil],
].inject([]) do |h, (format, direction, arraycoder)|
h[format] ||= {}
h[format][direction] = CoderMap.new result, CODERS_BY_NAME[format][direction], format, arraycoder
h
end
end

def each_format(direction)
@maps.map { |f| f[direction] }
end

def map_for(format, direction)
@maps[format][direction]
end
end
module_function :build_coder_maps

ValidFormats = { 0 => true, 1 => true }
ValidDirections = { :encoder => true, :decoder => true }
Expand All @@ -116,9 +141,7 @@ def check_format_and_direction(format, direction)
end
protected :check_format_and_direction

# The key of this hash maps to the `typname` column from the table.
# encoder_map is then dynamically built with oids as the key and Type
# objects as values.
# The key of these hashs maps to the `typname` column from the table pg_type.
CODERS_BY_NAME = []

# Register an encoder or decoder instance for casting a PostgreSQL type.
Expand Down Expand Up @@ -287,15 +310,15 @@ def typecast_result_value(result, _tuple, field)
end
end

def initialize(connection, coder_maps: nil)
@coder_maps = coder_maps || build_coder_maps(connection)
def initialize(connection_or_coder_maps)
@coder_maps = connection_or_coder_maps.is_a?(CoderMapsBundle) ? connection_or_coder_maps : CoderMapsBundle.new(connection_or_coder_maps)

# Populate TypeMapByOid hash with decoders
@coder_maps.flat_map{|f| f[:decoder].coders }.each do |coder|
@coder_maps.each_format(:decoder).flat_map{|f| f.coders }.each do |coder|
add_coder(coder)
end

typenames = @coder_maps.map{|f| f[:decoder].typenames_by_oid }
typenames = @coder_maps.each_format(:decoder).map{|f| f.typenames_by_oid }
self.default_type_map = WarningTypeMap.new(typenames)
end
end
Expand Down Expand Up @@ -333,11 +356,11 @@ def initialize(connection, coder_maps: nil)
class PG::BasicTypeMapBasedOnResult < PG::TypeMapByOid
include PG::BasicTypeRegistry

def initialize(connection)
@coder_maps = build_coder_maps(connection)
def initialize(connection_or_coder_maps)
@coder_maps = connection_or_coder_maps.is_a?(CoderMapsBundle) ? connection_or_coder_maps : CoderMapsBundle.new(connection_or_coder_maps)

# Populate TypeMapByOid hash with encoders
@coder_maps.flat_map{|f| f[:encoder].coders }.each do |coder|
@coder_maps.each_format(:encoder).flat_map{|f| f.coders }.each do |coder|
add_coder(coder)
end
end
Expand Down Expand Up @@ -379,8 +402,8 @@ class BinaryData < String

include PG::BasicTypeRegistry

def initialize(connection, coder_maps: nil)
@coder_maps = coder_maps || build_coder_maps(connection)
def initialize(connection_or_coder_maps)
@coder_maps = connection_or_coder_maps.is_a?(CoderMapsBundle) ? connection_or_coder_maps : CoderMapsBundle.new(connection_or_coder_maps)
@array_encoders_by_klass = array_encoders_by_klass
@encode_array_as = :array
init_encoders
Expand Down Expand Up @@ -424,7 +447,7 @@ def init_encoders

def coder_by_name(format, direction, name)
check_format_and_direction(format, direction)
@coder_maps[format][direction].coder_by_name(name)
@coder_maps.map_for(format, direction).coder_by_name(name)
end

def populate_encoder_list
Expand Down
26 changes: 18 additions & 8 deletions spec/pg/basic_type_mapping_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ def restore_type(types)
PG::BasicTypeMapForQueries.new @conn
end

it "can be initialized with a CoderMapsBundle instead of a connection" do
maps = PG::BasicTypeRegistry::CoderMapsBundle.new(@conn)
tm = PG::BasicTypeMapForQueries.new(maps)
expect( tm[Integer] ).to be_kind_of(PG::TextEncoder::Integer)
end

#
# Encoding Examples
#
Expand Down Expand Up @@ -221,6 +227,12 @@ def to_s
PG::BasicTypeMapForResults.new @conn
end

it "can be initialized with a CoderMapsBundle instead of a connection" do
maps = PG::BasicTypeRegistry::CoderMapsBundle.new(@conn)
tm = PG::BasicTypeMapForResults.new(maps)
expect( tm.rm_coder(0, 16) ).to be_kind_of(PG::TextDecoder::Boolean)
end

#
# Decoding Examples
#
Expand Down Expand Up @@ -569,6 +581,12 @@ def to_s
PG::BasicTypeMapBasedOnResult.new @conn
end

it "can be initialized with a CoderMapsBundle instead of a connection" do
maps = PG::BasicTypeRegistry::CoderMapsBundle.new(@conn)
tm = PG::BasicTypeMapBasedOnResult.new(maps)
expect( tm.rm_coder(0, 16) ).to be_kind_of(PG::TextEncoder::Boolean)
end

context "with usage of result oids for bind params encoder selection" do
it "can type cast query params" do
@conn.exec( "CREATE TEMP TABLE copytable (t TEXT, i INT, ai INT[])" )
Expand Down Expand Up @@ -635,12 +653,4 @@ def to_s
end
end
end

describe PG::BasicTypeRegistry do
it "can build coder maps" do
expect do
described_class.build_coder_maps(@conn)
end.to_not raise_error
end
end
end

0 comments on commit 4c6c6b2

Please sign in to comment.