Skip to content

Commit 538b46d

Browse files
committed
more typemap work on postgresql
1 parent d3a58f2 commit 538b46d

File tree

3 files changed

+208
-112
lines changed

3 files changed

+208
-112
lines changed

lib/arjdbc/postgresql/adapter.rb

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -798,11 +798,96 @@ def jdbc_connection_class(spec)
798798

799799
private
800800

801-
# Prepared statements aren't schema aware so we need to make sure we
802-
# store different PreparedStatement objects for different schemas
801+
FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
802+
803+
def execute_and_clear(sql, name, binds, prepare: false, async: false)
804+
sql = transform_query(sql)
805+
check_if_write_query(sql)
806+
807+
if !prepare || without_prepared_statement?(binds)
808+
result = exec_no_cache(sql, name, binds, async: async)
809+
else
810+
result = exec_cache(sql, name, binds, async: async)
811+
end
812+
begin
813+
ret = yield result
814+
ensure
815+
# Is this really result in AR PG?
816+
# result.clear
817+
end
818+
ret
819+
end
820+
821+
def exec_no_cache(sql, name, binds, async: false)
822+
materialize_transactions
823+
mark_transaction_written_if_write(sql)
824+
825+
# make sure we carry over any changes to ActiveRecord.default_timezone that have been
826+
# made since we established the connection
827+
update_typemap_for_default_timezone
828+
829+
type_casted_binds = type_casted_binds(binds)
830+
log(sql, name, binds, type_casted_binds, async: async) do
831+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
832+
@connection.exec_params(sql, type_casted_binds)
833+
end
834+
end
835+
end
836+
837+
def exec_cache(sql, name, binds, async: false)
838+
materialize_transactions
839+
mark_transaction_written_if_write(sql)
840+
update_typemap_for_default_timezone
841+
842+
stmt_key = prepare_statement(sql, binds)
843+
type_casted_binds = type_casted_binds(binds)
844+
845+
log(sql, name, binds, type_casted_binds, stmt_key, async: async) do
846+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
847+
@connection.exec_prepared(stmt_key, type_casted_binds)
848+
end
849+
end
850+
rescue ActiveRecord::StatementInvalid => e
851+
raise unless is_cached_plan_failure?(e)
852+
853+
# Nothing we can do if we are in a transaction because all commands
854+
# will raise InFailedSQLTransaction
855+
if in_transaction?
856+
raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
857+
else
858+
@lock.synchronize do
859+
# outside of transactions we can simply flush this query and retry
860+
@statements.delete sql_key(sql)
861+
end
862+
retry
863+
end
864+
end
865+
866+
# Annoyingly, the code for prepared statements whose return value may
867+
# have changed is FEATURE_NOT_SUPPORTED.
868+
#
869+
# This covers various different error types so we need to do additional
870+
# work to classify the exception definitively as a
871+
# ActiveRecord::PreparedStatementCacheExpired
872+
#
873+
# Check here for more details:
874+
# https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
875+
def is_cached_plan_failure?(e)
876+
pgerror = e.cause
877+
pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE) == FEATURE_NOT_SUPPORTED &&
878+
pgerror.result.result_error_field(PG::PG_DIAG_SOURCE_FUNCTION) == "RevalidateCachedQuery"
879+
rescue
880+
false
881+
end
882+
883+
def in_transaction?
884+
open_transactions > 0
885+
end
886+
887+
# Returns the statement identifier for the client side cache
888+
# of statements
803889
def sql_key(sql)
804890
"#{schema_search_path}-#{sql}"
805891
end
806-
807892
end
808893
end

lib/arjdbc/postgresql/oid_types.rb

Lines changed: 115 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,10 @@ def extensions
9191
end
9292

9393
def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc:
94-
if !type_map.key?(oid)
95-
load_additional_types(type_map, oid)
96-
end
94+
# This is unhappy with oid of regproc
95+
#if !type_map.key?(oid)
96+
# load_additional_types([oid])
97+
#end
9798

9899
type_map.fetch(oid, fmod, sql_type) {
99100
warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
@@ -103,15 +104,92 @@ def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc:
103104
}
104105
end
105106

107+
def reload_type_map
108+
type_map.clear
109+
initialize_type_map
110+
end
111+
112+
def initialize_type_map_inner(m)
113+
m.register_type "int2", Type::Integer.new(limit: 2)
114+
m.register_type "int4", Type::Integer.new(limit: 4)
115+
m.register_type "int8", Type::Integer.new(limit: 8)
116+
m.register_type "oid", OID::Oid.new
117+
m.register_type "float4", Type::Float.new
118+
m.alias_type "float8", "float4"
119+
m.register_type "text", Type::Text.new
120+
register_class_with_limit m, "varchar", Type::String
121+
m.alias_type "char", "varchar"
122+
m.alias_type "name", "varchar"
123+
m.alias_type "bpchar", "varchar"
124+
m.register_type "bool", Type::Boolean.new
125+
register_class_with_limit m, "bit", OID::Bit
126+
register_class_with_limit m, "varbit", OID::BitVarying
127+
m.register_type "date", OID::Date.new
128+
129+
m.register_type "money", OID::Money.new
130+
m.register_type "bytea", OID::Bytea.new
131+
m.register_type "point", OID::Point.new
132+
m.register_type "hstore", OID::Hstore.new
133+
m.register_type "json", Type::Json.new
134+
m.register_type "jsonb", OID::Jsonb.new
135+
m.register_type "cidr", OID::Cidr.new
136+
m.register_type "inet", OID::Inet.new
137+
m.register_type "uuid", OID::Uuid.new
138+
m.register_type "xml", OID::Xml.new
139+
m.register_type "tsvector", OID::SpecializedString.new(:tsvector)
140+
m.register_type "macaddr", OID::Macaddr.new
141+
m.register_type "citext", OID::SpecializedString.new(:citext)
142+
m.register_type "ltree", OID::SpecializedString.new(:ltree)
143+
m.register_type "line", OID::SpecializedString.new(:line)
144+
m.register_type "lseg", OID::SpecializedString.new(:lseg)
145+
m.register_type "box", OID::SpecializedString.new(:box)
146+
m.register_type "path", OID::SpecializedString.new(:path)
147+
m.register_type "polygon", OID::SpecializedString.new(:polygon)
148+
m.register_type "circle", OID::SpecializedString.new(:circle)
149+
150+
register_class_with_precision m, "time", Type::Time
151+
register_class_with_precision m, "timestamp", OID::Timestamp
152+
register_class_with_precision m, "timestamptz", OID::TimestampWithTimeZone
153+
154+
m.register_type "numeric" do |_, fmod, sql_type|
155+
precision = extract_precision(sql_type)
156+
scale = extract_scale(sql_type)
157+
158+
# The type for the numeric depends on the width of the field,
159+
# so we'll do something special here.
160+
#
161+
# When dealing with decimal columns:
162+
#
163+
# places after decimal = fmod - 4 & 0xffff
164+
# places before decimal = (fmod - 4) >> 16 & 0xffff
165+
if fmod && (fmod - 4 & 0xffff).zero?
166+
# FIXME: Remove this class, and the second argument to
167+
# lookups on PG
168+
Type::DecimalWithoutScale.new(precision: precision)
169+
else
170+
OID::Decimal.new(precision: precision, scale: scale)
171+
end
172+
end
173+
174+
m.register_type "interval" do |*args, sql_type|
175+
precision = extract_precision(sql_type)
176+
OID::Interval.new(precision: precision)
177+
end
178+
179+
# pgjdbc returns these if the column is auto-incrmenting
180+
m.alias_type 'serial', 'int4'
181+
m.alias_type 'bigserial', 'int8'
182+
end
183+
184+
185+
# We differ from AR here because we will initialize type_map when adapter initializes
106186
def type_map
107187
@type_map
108188
end
109189

110-
def reload_type_map
111-
if ( @type_map ||= nil )
112-
@type_map.clear
113-
initialize_type_map(@type_map)
114-
end
190+
def initialize_type_map(m = type_map)
191+
initialize_type_map_inner(m)
192+
load_additional_types
115193
end
116194

117195
private
@@ -124,117 +202,45 @@ def register_class_with_precision(...)
124202
::ActiveRecord::ConnectionAdapters::AbstractAdapter.send(:register_class_with_precision, ...)
125203
end
126204

127-
def initialize_type_map(m = type_map)
128-
m.register_type "int2", Type::Integer.new(limit: 2)
129-
m.register_type "int4", Type::Integer.new(limit: 4)
130-
m.register_type "int8", Type::Integer.new(limit: 8)
131-
m.register_type "oid", OID::Oid.new
132-
m.register_type "float4", Type::Float.new
133-
m.alias_type "float8", "float4"
134-
m.register_type "text", Type::Text.new
135-
register_class_with_limit m, "varchar", Type::String
136-
m.alias_type "char", "varchar"
137-
m.alias_type "name", "varchar"
138-
m.alias_type "bpchar", "varchar"
139-
m.register_type "bool", Type::Boolean.new
140-
register_class_with_limit m, "bit", OID::Bit
141-
register_class_with_limit m, "varbit", OID::BitVarying
142-
m.register_type "date", OID::Date.new
143-
144-
m.register_type "money", OID::Money.new
145-
m.register_type "bytea", OID::Bytea.new
146-
m.register_type "point", OID::Point.new
147-
m.register_type "hstore", OID::Hstore.new
148-
m.register_type "json", Type::Json.new
149-
m.register_type "jsonb", OID::Jsonb.new
150-
m.register_type "cidr", OID::Cidr.new
151-
m.register_type "inet", OID::Inet.new
152-
m.register_type "uuid", OID::Uuid.new
153-
m.register_type "xml", OID::Xml.new
154-
m.register_type "tsvector", OID::SpecializedString.new(:tsvector)
155-
m.register_type "macaddr", OID::Macaddr.new
156-
m.register_type "citext", OID::SpecializedString.new(:citext)
157-
m.register_type "ltree", OID::SpecializedString.new(:ltree)
158-
m.register_type "line", OID::SpecializedString.new(:line)
159-
m.register_type "lseg", OID::SpecializedString.new(:lseg)
160-
m.register_type "box", OID::SpecializedString.new(:box)
161-
m.register_type "path", OID::SpecializedString.new(:path)
162-
m.register_type "polygon", OID::SpecializedString.new(:polygon)
163-
m.register_type "circle", OID::SpecializedString.new(:circle)
164-
165-
register_class_with_precision m, "time", Type::Time
166-
register_class_with_precision m, "timestamp", OID::Timestamp
167-
register_class_with_precision m, "timestamptz", OID::TimestampWithTimeZone
168-
169-
m.register_type "numeric" do |_, fmod, sql_type|
170-
precision = extract_precision(sql_type)
171-
scale = extract_scale(sql_type)
172-
173-
# The type for the numeric depends on the width of the field,
174-
# so we'll do something special here.
175-
#
176-
# When dealing with decimal columns:
177-
#
178-
# places after decimal = fmod - 4 & 0xffff
179-
# places before decimal = (fmod - 4) >> 16 & 0xffff
180-
if fmod && (fmod - 4 & 0xffff).zero?
181-
# FIXME: Remove this class, and the second argument to
182-
# lookups on PG
183-
Type::DecimalWithoutScale.new(precision: precision)
184-
else
185-
OID::Decimal.new(precision: precision, scale: scale)
205+
def load_additional_types(oids = nil) # :nodoc:
206+
initializer = ArjdbcTypeMapInitializer.new(type_map)
207+
load_types_queries(initializer, oids) do |query|
208+
execute_and_clear(query, "SCHEMA", []) do |records|
209+
initializer.run(records)
186210
end
187211
end
188-
189-
m.register_type "interval" do |*args, sql_type|
190-
precision = extract_precision(sql_type)
191-
OID::Interval.new(precision: precision)
192-
end
193-
194-
# pgjdbc returns these if the column is auto-incrmenting
195-
m.alias_type 'serial', 'int4'
196-
m.alias_type 'bigserial', 'int8'
197212
end
198213

199-
def load_additional_types(type_map, oid = nil) # :nodoc:
200-
initializer = ArjdbcTypeMapInitializer.new(type_map)
201-
202-
if supports_ranges?
203-
query = <<-SQL
204-
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype,
205-
ns.nspname, ns.nspname = ANY(current_schemas(true)) in_ns
206-
FROM pg_type as t
207-
LEFT JOIN pg_range as r ON oid = rngtypid
208-
JOIN pg_namespace AS ns ON t.typnamespace = ns.oid
209-
SQL
214+
def load_types_queries(initializer, oids)
215+
query = <<~SQL
216+
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
217+
FROM pg_type as t
218+
LEFT JOIN pg_range as r ON oid = rngtypid
219+
SQL
220+
if oids
221+
yield query + "WHERE t.oid IN (%s)" % oids.join(", ")
210222
else
211-
query = <<-SQL
212-
SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype,
213-
ns.nspname, ns.nspname = ANY(current_schemas(true)) in_ns
214-
FROM pg_type as t
215-
JOIN pg_namespace AS ns ON t.typnamespace = ns.oid
216-
SQL
223+
yield query + initializer.query_conditions_for_known_type_names
224+
yield query + initializer.query_conditions_for_known_type_types
225+
yield query + initializer.query_conditions_for_array_types
217226
end
227+
end
218228

219-
if oid
220-
if oid.is_a? Numeric || oid.match(/^\d+$/)
221-
# numeric OID
222-
query += "WHERE t.oid = %s" % oid
229+
def update_typemap_for_default_timezone
230+
if @default_timezone != ActiveRecord.default_timezone && @timestamp_decoder
231+
decoder_class = ActiveRecord.default_timezone == :utc ?
232+
PG::TextDecoder::TimestampUtc :
233+
PG::TextDecoder::TimestampWithoutTimeZone
223234

224-
elsif m = oid.match(/"?(\w+)"?\."?(\w+)"?/)
225-
# namespace and type name
226-
query += "WHERE ns.nspname = '%s' AND t.typname = '%s'" % [m[1], m[2]]
235+
@timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h)
236+
@connection.type_map_for_results.add_coder(@timestamp_decoder)
227237

228-
else
229-
# only type name
230-
query += "WHERE t.typname = '%s' AND ns.nspname = ANY(current_schemas(true))" % oid
231-
end
232-
else
233-
query += initializer.query_conditions_for_initial_load
234-
end
238+
@default_timezone = ActiveRecord.default_timezone
235239

236-
records = execute(query, 'SCHEMA')
237-
initializer.run(records)
240+
# if default timezone has changed, we need to reconfigure the connection
241+
# (specifically, the session time zone)
242+
configure_connection
243+
end
238244
end
239245

240246
def extract_scale(sql_type)

src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ public IRubyObject database_product(final ThreadContext context) {
214214
});
215215
}
216216

217+
@JRubyMethod
218+
public IRubyObject exec_params(ThreadContext context, IRubyObject sql, IRubyObject binds) {
219+
return execute_prepared_query(context, sql, binds, null);
220+
}
221+
217222
private transient RubyClass oidArray; // PostgreSQL::OID::Array
218223

219224
private RubyClass oidArray(final ThreadContext context) {

0 commit comments

Comments
 (0)