Skip to content

Commit 000bfb4

Browse files
authored
Usage of bind variables for volatile filter conditions (3) (#2125)
* Usage of bind variables for volatile filter conditions * Fix rubocop issues
1 parent 6ca448d commit 000bfb4

File tree

5 files changed

+91
-39
lines changed

5 files changed

+91
-39
lines changed

lib/active_record/connection_adapters/oracle_enhanced/connection.rb

+20-11
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,25 @@ def describe(name)
3636
sql = <<~SQL.squish
3737
SELECT owner, table_name, 'TABLE' name_type
3838
FROM all_tables
39-
WHERE owner = '#{table_owner}'
40-
AND table_name = '#{table_name}'
39+
WHERE owner = :table_owner
40+
AND table_name = :table_name
4141
UNION ALL
4242
SELECT owner, view_name table_name, 'VIEW' name_type
4343
FROM all_views
44-
WHERE owner = '#{table_owner}'
45-
AND view_name = '#{table_name}'
44+
WHERE owner = :table_owner
45+
AND view_name = :table_name
4646
UNION ALL
4747
SELECT table_owner, table_name, 'SYNONYM' name_type
4848
FROM all_synonyms
49-
WHERE owner = '#{table_owner}'
50-
AND synonym_name = '#{table_name}'
49+
WHERE owner = :table_owner
50+
AND synonym_name = :table_name
5151
UNION ALL
5252
SELECT table_owner, table_name, 'SYNONYM' name_type
5353
FROM all_synonyms
5454
WHERE owner = 'PUBLIC'
55-
AND synonym_name = '#{real_name}'
55+
AND synonym_name = :real_name
5656
SQL
57-
if result = _select_one(sql)
57+
if result = _select_one(sql, "CONNECTION", [table_owner, table_name, table_owner, table_name, table_owner, table_name, real_name])
5858
case result["name_type"]
5959
when "SYNONYM"
6060
describe("#{result['owner'] && "#{result['owner']}."}#{result['table_name']}")
@@ -92,14 +92,23 @@ def _oracle_downcase(column_name)
9292

9393
# Returns a record hash with the column names as keys and column values
9494
# as values.
95+
# binds is a array of native values in contrast to ActiveRecord::Relation::QueryAttribute
9596
def _select_one(arel, name = nil, binds = [])
96-
result = select(arel)
97-
result.first if result
97+
cursor = prepare(arel)
98+
cursor.bind_params(binds)
99+
cursor.exec
100+
columns = cursor.get_col_names.map do |col_name|
101+
_oracle_downcase(col_name)
102+
end
103+
row = cursor.fetch
104+
columns.each_with_index.map { |x, i| [x, row[i]] }.to_h if row
105+
ensure
106+
cursor.close
98107
end
99108

100109
# Returns a single value from a record
101110
def _select_value(arel, name = nil, binds = [])
102-
if result = _select_one(arel)
111+
if result = _select_one(arel, name, binds)
103112
result.values.first
104113
end
105114
end

lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -367,12 +367,12 @@ def index_name(table_name, options) #:nodoc:
367367
# Will always query database and not index cache.
368368
def index_name_exists?(table_name, index_name)
369369
(_owner, table_name) = @connection.describe(table_name)
370-
result = select_value(<<~SQL.squish, "SCHEMA")
370+
result = select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name), bind_string("index_name", index_name.to_s.upcase)])
371371
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ 1 FROM all_indexes i
372372
WHERE i.owner = SYS_CONTEXT('userenv', 'current_schema')
373373
AND i.table_owner = SYS_CONTEXT('userenv', 'current_schema')
374-
AND i.table_name = '#{table_name}'
375-
AND i.index_name = '#{index_name.to_s.upcase}'
374+
AND i.table_name = :table_name
375+
AND i.index_name = :index_name
376376
SQL
377377
result == 1
378378
end

lib/active_record/connection_adapters/oracle_enhanced/structure_dump.rb

+9-9
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ def structure_dump #:nodoc:
3030
tables.each do |table_name|
3131
virtual_columns = virtual_columns_for(table_name) if supports_virtual_columns?
3232
ddl = +"CREATE#{ ' GLOBAL TEMPORARY' if temporary_table?(table_name)} TABLE \"#{table_name}\" (\n"
33-
columns = select_all(<<~SQL.squish, "SCHEMA")
33+
columns = select_all(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)])
3434
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ column_name, data_type, data_length, char_used, char_length,
3535
data_precision, data_scale, data_default, nullable
3636
FROM all_tab_columns
37-
WHERE table_name = '#{table_name}'
37+
WHERE table_name = :table_name
3838
AND owner = SYS_CONTEXT('userenv', 'current_schema')
3939
ORDER BY column_id
4040
SQL
@@ -174,10 +174,10 @@ def structure_dump_table_comments(table_name)
174174

175175
def structure_dump_column_comments(table_name)
176176
comments = []
177-
columns = select_values(<<~SQL.squish, "SCHEMA")
177+
columns = select_values(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)])
178178
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ column_name FROM all_tab_columns
179179
WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
180-
AND table_name = '#{table_name}' ORDER BY column_id
180+
AND table_name = :table_name ORDER BY column_id
181181
SQL
182182

183183
columns.each do |column|
@@ -218,11 +218,11 @@ def structure_dump_db_stored_code #:nodoc:
218218
SQL
219219
all_source.each do |source|
220220
ddl = +"CREATE OR REPLACE \n"
221-
texts = select_all(<<~SQL.squish, "all source at structure dump")
221+
texts = select_all(<<~SQL.squish, "all source at structure dump", [bind_string("source_name", source["name"]), bind_string("source_type", source["type"])])
222222
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ text
223223
FROM all_source
224-
WHERE name = '#{source['name']}'
225-
AND type = '#{source['type']}'
224+
WHERE name = :source_name
225+
AND type = :source_type
226226
AND owner = SYS_CONTEXT('userenv', 'current_schema')
227227
ORDER BY line
228228
SQL
@@ -323,12 +323,12 @@ def execute_structure_dump(string)
323323
# Called only if `supports_virtual_columns?` returns true
324324
# return [{'column_name' => 'FOOS', 'data_default' => '...'}, ...]
325325
def virtual_columns_for(table)
326-
select_all(<<~SQL.squish, "SCHEMA")
326+
select_all(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table.upcase)])
327327
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ column_name, data_default
328328
FROM all_tab_cols
329329
WHERE virtual_column = 'YES'
330330
AND owner = SYS_CONTEXT('userenv', 'current_schema')
331-
AND table_name = '#{table.upcase}'
331+
AND table_name = :table_name
332332
SQL
333333
end
334334

lib/active_record/connection_adapters/oracle_enhanced_adapter.rb

+27-11
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ def default_tablespace
562562
def column_definitions(table_name)
563563
(owner, desc_table_name) = @connection.describe(table_name)
564564

565-
select_all(<<~SQL.squish, "SCHEMA")
565+
select_all(<<~SQL.squish, "SCHEMA", [bind_string("owner", owner), bind_string("table_name", desc_table_name)])
566566
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ cols.column_name AS name, cols.data_type AS sql_type,
567567
cols.data_default, cols.nullable, cols.virtual_column, cols.hidden_column,
568568
cols.data_type_owner AS sql_type_owner,
@@ -575,8 +575,8 @@ def column_definitions(table_name)
575575
DECODE(data_type, 'NUMBER', data_scale, NULL) AS scale,
576576
comments.comments as column_comment
577577
FROM all_tab_cols cols, all_col_comments comments
578-
WHERE cols.owner = '#{owner}'
579-
AND cols.table_name = #{quote(desc_table_name)}
578+
WHERE cols.owner = :owner
579+
AND cols.table_name = :table_name
580580
AND cols.hidden_column = 'NO'
581581
AND cols.owner = comments.owner
582582
AND cols.table_name = comments.table_name
@@ -594,19 +594,19 @@ def clear_table_columns_cache(table_name)
594594
def pk_and_sequence_for(table_name, owner = nil, desc_table_name = nil) #:nodoc:
595595
(owner, desc_table_name) = @connection.describe(table_name)
596596

597-
seqs = select_values(<<~SQL.squish, "SCHEMA")
597+
seqs = select_values_forcing_binds(<<~SQL.squish, "SCHEMA", [bind_string("owner", owner), bind_string("sequence_name", default_sequence_name(desc_table_name))])
598598
select /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ us.sequence_name
599599
from all_sequences us
600-
where us.sequence_owner = '#{owner}'
601-
and us.sequence_name = upper(#{quote(default_sequence_name(desc_table_name))})
600+
where us.sequence_owner = :owner
601+
and us.sequence_name = upper(:sequence_name)
602602
SQL
603603

604604
# changed back from user_constraints to all_constraints for consistency
605-
pks = select_values(<<~SQL.squish, "SCHEMA")
605+
pks = select_values_forcing_binds(<<~SQL.squish, "SCHEMA", [bind_string("owner", owner), bind_string("table_name", desc_table_name)])
606606
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ cc.column_name
607607
FROM all_constraints c, all_cons_columns cc
608-
WHERE c.owner = '#{owner}'
609-
AND c.table_name = #{quote(desc_table_name)}
608+
WHERE c.owner = :owner
609+
AND c.table_name = :table_name
610610
AND c.constraint_type = 'P'
611611
AND cc.owner = c.owner
612612
AND cc.constraint_name = c.constraint_name
@@ -636,7 +636,7 @@ def has_primary_key?(table_name, owner = nil, desc_table_name = nil) #:nodoc:
636636
def primary_keys(table_name) # :nodoc:
637637
(_owner, desc_table_name) = @connection.describe(table_name)
638638

639-
pks = select_values(<<~SQL.squish, "SCHEMA", [bind_string("table_name", desc_table_name)])
639+
pks = select_values_forcing_binds(<<~SQL.squish, "SCHEMA", [bind_string("table_name", desc_table_name)])
640640
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */ cc.column_name
641641
FROM all_constraints c, all_cons_columns cc
642642
WHERE c.owner = SYS_CONTEXT('userenv', 'current_schema')
@@ -666,7 +666,7 @@ def columns_for_distinct(columns, orders) #:nodoc:
666666
end
667667

668668
def temporary_table?(table_name) #:nodoc:
669-
select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name.upcase)]) == "Y"
669+
select_value_forcing_binds(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name.upcase)]) == "Y"
670670
SELECT /*+ OPTIMIZER_FEATURES_ENABLE('11.2.0.2') */
671671
temporary FROM all_tables WHERE table_name = :table_name and owner = SYS_CONTEXT('userenv', 'current_schema')
672672
SQL
@@ -764,6 +764,22 @@ def bind_string(name, value)
764764
ActiveRecord::Relation::QueryAttribute.new(name, value, Type::OracleEnhanced::String.new)
765765
end
766766

767+
# call select_values using binds even if surrounding SQL preparation/execution is done + # with conn.unprepared_statement (like AR.to_sql)
768+
def select_values_forcing_binds(arel, name, binds)
769+
# remove possible force of unprepared SQL during dictionary access
770+
unprepared_statement_forced = prepared_statements_disabled_cache.include?(object_id)
771+
prepared_statements_disabled_cache.delete(object_id) if unprepared_statement_forced
772+
773+
select_values(arel, name, binds)
774+
ensure
775+
# Restore unprepared_statement setting for surrounding SQL
776+
prepared_statements_disabled_cache.add(object_id) if unprepared_statement_forced
777+
end
778+
779+
def select_value_forcing_binds(arel, name, binds)
780+
single_value_from_rows(select_values_forcing_binds(arel, name, binds))
781+
end
782+
767783
ActiveRecord::Type.register(:boolean, Type::OracleEnhanced::Boolean, adapter: :oracleenhanced)
768784
ActiveRecord::Type.register(:json, Type::OracleEnhanced::Json, adapter: :oracleenhanced)
769785
end

spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb

+32-5
Original file line numberDiff line numberDiff line change
@@ -577,9 +577,29 @@ class TestLog < ActiveRecord::Base
577577
schema_define do
578578
drop_table :test_posts, if_exists: true
579579
create_table :test_posts
580+
581+
drop_table :users, if_exists: true
582+
create_table :users, force: true do |t|
583+
t.string :name
584+
t.integer :group_id
585+
end
586+
587+
drop_table :groups, if_exists: true
588+
create_table :groups, force: true do |t|
589+
t.string :name
590+
end
580591
end
592+
581593
class ::TestPost < ActiveRecord::Base
582594
end
595+
596+
class User < ActiveRecord::Base
597+
belongs_to :group
598+
end
599+
600+
class Group < ActiveRecord::Base
601+
has_one :user
602+
end
583603
end
584604

585605
before(:each) do
@@ -594,6 +614,8 @@ class ::TestPost < ActiveRecord::Base
594614
after(:all) do
595615
schema_define do
596616
drop_table :test_posts
617+
drop_table :users
618+
drop_table :groups
597619
end
598620
Object.send(:remove_const, "TestPost")
599621
ActiveRecord::Base.clear_cache!
@@ -610,22 +632,27 @@ class ::TestPost < ActiveRecord::Base
610632
expect(@logger.logged(:debug).last).to match(/\["table_name", "TEST_POSTS"\]/)
611633
end
612634

613-
it "should return content from columns without bind usage" do
635+
it "should return content from columns witt bind usage" do
614636
expect(@conn.columns("TEST_POSTS").length).to be > 0
615-
expect(@logger.logged(:debug).last).not_to match(/:table_name/)
616-
expect(@logger.logged(:debug).last).not_to match(/\["table_name", "TEST_POSTS"\]/)
637+
expect(@logger.logged(:debug).last).to match(/:table_name/)
638+
expect(@logger.logged(:debug).last).to match(/\["table_name", "TEST_POSTS"\]/)
617639
end
618640

619-
it "should return pk and sequence from pk_and_sequence_for without bind usage" do
641+
it "should return pk and sequence from pk_and_sequence_for with bind usage" do
620642
expect(@conn.pk_and_sequence_for("TEST_POSTS").length).to eq 2
621-
expect(@logger.logged(:debug).last).not_to match(/\["table_name", "TEST_POSTS"\]/)
643+
expect(@logger.logged(:debug).last).to match(/\["table_name", "TEST_POSTS"\]/)
622644
end
623645

624646
it "should return pk from primary_keys with bind usage" do
625647
expect(@conn.primary_keys("TEST_POSTS")).to eq ["id"]
626648
expect(@logger.logged(:debug).last).to match(/\["table_name", "TEST_POSTS"\]/)
627649
end
628650

651+
it "should not raise missing IN/OUT parameter like issue 1687 " do
652+
# "to_sql" enforces unprepared_statement including dictionary access SQLs
653+
expect { User.joins(:group).to_sql }.not_to raise_exception
654+
end
655+
629656
it "should return false from temporary_table? with bind usage" do
630657
expect(@conn.temporary_table?("TEST_POSTS")).to eq false
631658
expect(@logger.logged(:debug).last).to match(/:table_name/)

0 commit comments

Comments
 (0)