Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions ext/pg_type_map.c
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,36 @@ pg_typemap_s_allocate( VALUE klass )
return self;
}


static VALUE
pg_typemap_query_param_encoders( VALUE self, VALUE params )
{
t_typemap *this = RTYPEDDATA_DATA( self );
int nParams;
int i=0;
VALUE res;

Check_Type(params, T_ARRAY);

this->funcs.fit_to_query( self, params );

nParams = RARRAY_LENINT(params);
res = rb_ary_new();

for ( i = 0; i < nParams; i++ ) {
t_pg_coder *conv;
VALUE param_value = rb_ary_entry(params, i);

/* Let the given typemap select a coder for this param */
conv = this->funcs.typecast_query_param(this, param_value, i);
if(conv)
rb_ary_push(res, conv->coder_obj);
else
rb_ary_push(res, Qnil);
}
return res;
}

/*
* call-seq:
* res.default_type_map = typemap
Expand Down Expand Up @@ -194,6 +224,7 @@ init_pg_type_map(void)
*/
rb_cTypeMap = rb_define_class_under( rb_mPG, "TypeMap", rb_cObject );
rb_define_alloc_func( rb_cTypeMap, pg_typemap_s_allocate );
rb_define_method( rb_cTypeMap, "query_param_encoders", pg_typemap_query_param_encoders, 1 );

rb_mDefaultTypeMappable = rb_define_module_under( rb_cTypeMap, "DefaultTypeMappable");
rb_define_method( rb_mDefaultTypeMappable, "default_type_map=", pg_typemap_default_type_map_set, 1 );
Expand Down
49 changes: 49 additions & 0 deletions lib/pg/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,55 @@ def cancel
end
alias async_cancel cancel

PLACEHOLDER_RE = /
'(?:''|[^'])*' | # string literal
"(?:""|[^"])*" | # quoted identifier
--[^\n]* | # line comment
\/\*.*?\*\/ | # block comment
\$\$.*?\$\$ | # dollar-quoted string. E.g. $$ $1 $$
\$(?<__dq_tag>[A-Za-z_][A-Za-z_0-9]*)\$.*?\$\k<__dq_tag>\$ | # named dollar-quoted string. E.g. $foo$ $1 $foo$
(?<placeholder>\$(?:[1-9]\d*)) # placeholder we are interested in
/mx
private_constant :PLACEHOLDER_RE

# call-seq:
# conn.compile( sql, params ) -> String
#
# Compiles your prepared sql statement and the given positional arguments into plain sql.
# Example:
# res = conn.exec_params('SELECT $1 AS a, $2 AS b, $3 AS c', [1, 2, nil])
# # => "SELECT '1' AS a, '2' AS b, NULL AS c"
def compile(sql, params)
return sql if params.empty?

encoders = type_map_for_queries.query_param_encoders(params)
params = encoders.map.with_index do |enc, i|
value = params[i]
case value
when TrueClass, FalseClass
"#{value}"
when NilClass
'NULL'
when PG::BasicTypeMapForQueries::BinaryData
value = "'#{ PG::TextEncoder::Bytea.new.encode(value) }'"
else
if enc
raise ArgumentError, "binary encoded data from #{enc} cannot be inserted into SQL text" if enc.format != 0
value = enc.encode(value)
end
"'#{escape(value.to_s)}'"
end
end

sql.gsub(PLACEHOLDER_RE).each do |matched|
placeholder = Regexp.last_match[:placeholder]
# Do not replace non-positional args string and pass it as is
next matched unless placeholder

params[placeholder[1..].to_i - 1]
end
end

module Pollable
# Track the progress of the connection, waiting for the socket to become readable/writable before polling it.
#
Expand Down
105 changes: 105 additions & 0 deletions spec/pg/connection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3013,6 +3013,111 @@ def wait_check_socket(conn)
end
end

describe :compile do
describe "default type map" do
it "compiles prepared sql into plain sql" do
compiled = @conn.compile(<<~SQL, [1, "2", true, false, nil])
-- this is one: $1
/* this is another one: $1 */
select $1::int as a, $2 as b, $3 as c, $4 as d, $5 as e, '$5' as f, $$ $6 $$ as g, -- this is two: $2
$body$ $1 $body$ as h, t."$1", t."$2"
from (select 10 as "$1", 20 as "$2") as t
SQL

aggregate_failures do
expect(compiled).to include("-- this is one: $1")
expect(compiled).to include("/* this is another one: $1 */")
expect(compiled).to include("-- this is two: $2")
expect(@conn.exec(compiled).first).to(
eq(
"a" => "1",
"b" => "2",
"c" => "t",
"d" => "f",
"e" => nil,
"f" => "$5",
"g" => " $6 ",
"h" => " $1 ",
"$1" => "10",
"$2" => "20",
), compiled
)
end
end

it "escapes strings properly" do
compiled = @conn.compile(<<~SQL, ["', '1"])
select $1 as one
SQL

expect(@conn.exec(compiled).first).to(eq("one" => "', '1"))
end
end

describe "PG::TypeMapByClass type map" do
before do
@conn2 = PG.connect(@conninfo)
@conn2.type_map_for_queries = PG::BasicTypeMapForQueries.new(@conn2)
@conn2.type_map_for_results = PG::BasicTypeMapForResults.new(@conn2)
end

after do
@conn2.close
end

it "compiles prepared sql into plain sql" do
compiled = @conn2.compile(<<~SQL, [1, "2", { foo: :bar }, [1], true, false, nil])
-- this is one: $1
/* this is another one: $1 */
select $1::int as a, $2 as b, $3::json as c, $4::int[] as d, '$5' as e, $$ $6 $$ as f,
$body$ $1 $body$ as g, -- this is two: $2
t."$1", t."$2", $5 as h, $6 as i, $7 j
from (select 10 as "$1", 20 as "$2") as t
SQL

aggregate_failures do
expect(compiled).to include("-- this is one: $1")
expect(compiled).to include("/* this is another one: $1 */")
expect(compiled).to include("-- this is two: $2")
expect(@conn2.exec(compiled).first).to(
eq(
"a" => 1,
"b" => "2",
"c" => { "foo" => "bar" },
"d" => [1],
"e" => "$5",
"f" => " $6 ",
"g" => " $1 ",
"$1" => 10,
"$2" => 20,
"h" => true,
"i" => false,
"j" => nil
)
)
end
end

it "escapes strings properly" do
compiled = @conn2.compile(<<~SQL, ["', '1"])
select $1 as one
SQL

expect(@conn2.exec(compiled).first).to(eq("one" => "', '1"))
end

it "encodes binary strings properly" do
binary = PG::BasicTypeMapForQueries::BinaryData.new("\0\xff\r\n\t'".b)
compiled = @conn2.compile(<<~SQL, [binary])
select $1::bytea as one
SQL

res = @conn2.exec(compiled).first
expect(res["one"]).to(eq(binary))
end
end
end

describe "deprecated forms of methods" do
if PG::VERSION < "2"
it "should forward exec to exec_params" do
Expand Down
Loading