Skip to content

Commit 5c24399

Browse files
authored
fix: Reliably quote columns/tables (#1400)
* refactor: easily quote table/column * refactor: extract table name when missing * fix: Reliably quote columns/tables * refactor: putting quoting methods together * Handle special case of * - tests * fix: hack mysql test query comparison
1 parent 9156734 commit 5c24399

File tree

3 files changed

+73
-38
lines changed

3 files changed

+73
-38
lines changed

lib/jsonapi/active_relation_resource.rb

+35-10
Original file line numberDiff line numberDiff line change
@@ -800,18 +800,29 @@ def sort_records(records, order_options, options)
800800
apply_sort(records, order_options, options)
801801
end
802802

803+
def sql_field_with_alias(table, field, quoted = true)
804+
Arel.sql("#{concat_table_field(table, field, quoted)} AS #{alias_table_field(table, field, quoted)}")
805+
end
806+
803807
def concat_table_field(table, field, quoted = false)
804-
if table.blank? || field.to_s.include?('.')
808+
if table.blank?
809+
split_table, split_field = field.to_s.split('.')
810+
if split_table && split_field
811+
table = split_table
812+
field = split_field
813+
end
814+
end
815+
if table.blank?
805816
# :nocov:
806817
if quoted
807-
quote(field)
818+
quote_column_name(field)
808819
else
809820
field.to_s
810821
end
811822
# :nocov:
812823
else
813824
if quoted
814-
"#{quote(table)}.#{quote(field)}"
825+
"#{quote_table_name(table)}.#{quote_column_name(field)}"
815826
else
816827
# :nocov:
817828
"#{table.to_s}.#{field.to_s}"
@@ -820,32 +831,46 @@ def concat_table_field(table, field, quoted = false)
820831
end
821832
end
822833

823-
def sql_field_with_alias(table, field, quoted = true)
824-
Arel.sql("#{concat_table_field(table, field, quoted)} AS #{alias_table_field(table, field, quoted)}")
825-
end
826-
827834
def alias_table_field(table, field, quoted = false)
828835
if table.blank? || field.to_s.include?('.')
829836
# :nocov:
830837
if quoted
831-
quote(field)
838+
quote_column_name(field)
832839
else
833840
field.to_s
834841
end
835842
# :nocov:
836843
else
837844
if quoted
838845
# :nocov:
839-
quote("#{table.to_s}_#{field.to_s}")
846+
quote_column_name("#{table.to_s}_#{field.to_s}")
840847
# :nocov:
841848
else
842849
"#{table.to_s}_#{field.to_s}"
843850
end
844851
end
845852
end
846853

854+
def quote_table_name(table_name)
855+
if _model_class&.connection
856+
_model_class.connection.quote_table_name(table_name)
857+
else
858+
quote(table_name)
859+
end
860+
end
861+
862+
def quote_column_name(column_name)
863+
return column_name if column_name == "*"
864+
if _model_class&.connection
865+
_model_class.connection.quote_column_name(column_name)
866+
else
867+
quote(column_name)
868+
end
869+
end
870+
871+
# fallback quote identifier when database adapter not available
847872
def quote(field)
848-
"\"#{field.to_s}\""
873+
%{"#{field.to_s}"}
849874
end
850875

851876
def apply_filters(records, filters, options = {})

test/test_helper.rb

+29-6
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,29 @@ def run_in_transaction?
459459

460460
self.fixture_path = "#{Rails.root}/fixtures"
461461
fixtures :all
462+
463+
def adapter_name
464+
ActiveRecord::Base.connection.adapter_name
465+
end
466+
467+
def db_quote_identifier
468+
case adapter_name
469+
when 'SQLite', 'PostgreSQL'
470+
%{"}
471+
when 'Mysql2'
472+
%{`}
473+
else
474+
fail ArgumentError, "Unhandled adapter #{adapter_name} in #{__callee__}"
475+
end
476+
end
477+
478+
def db_true
479+
ActiveRecord::Base.connection.quote(true)
480+
end
481+
482+
def sql_for_compare(sql)
483+
sql.tr(db_quote_identifier, %{"})
484+
end
462485
end
463486

464487
class ActiveSupport::TestCase
@@ -501,8 +524,8 @@ def assert_cacheable_jsonapi_get(url, cached_classes = :all)
501524
end
502525

503526
assert_equal(
504-
non_caching_response.pretty_inspect,
505-
json_response.pretty_inspect,
527+
sql_for_compare(non_caching_response.pretty_inspect),
528+
sql_for_compare(json_response.pretty_inspect),
506529
"Cache warmup response must match normal response"
507530
)
508531

@@ -511,8 +534,8 @@ def assert_cacheable_jsonapi_get(url, cached_classes = :all)
511534
end
512535

513536
assert_equal(
514-
non_caching_response.pretty_inspect,
515-
json_response.pretty_inspect,
537+
sql_for_compare(non_caching_response.pretty_inspect),
538+
sql_for_compare(json_response.pretty_inspect),
516539
"Cached response must match normal response"
517540
)
518541
assert_equal 0, cached[:total][:misses], "Cached response must not cause any cache misses"
@@ -580,8 +603,8 @@ def assert_cacheable_get(action, **args)
580603
"Cache (mode: #{mode}) #{phase} response status must match normal response"
581604
)
582605
assert_equal(
583-
non_caching_response.pretty_inspect,
584-
json_response_sans_all_backtraces.pretty_inspect,
606+
sql_for_compare(non_caching_response.pretty_inspect),
607+
sql_for_compare(json_response_sans_all_backtraces.pretty_inspect),
585608
"Cache (mode: #{mode}) #{phase} response body must match normal response"
586609
)
587610
assert_operator(

test/unit/active_relation_resource_finder/join_manager_test.rb

+9-22
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,12 @@
33

44
class JoinTreeTest < ActiveSupport::TestCase
55

6-
def db_true
7-
case ActiveRecord::Base.connection.adapter_name
8-
when 'SQLite'
9-
if Rails::VERSION::MAJOR >= 6 || (Rails::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 2)
10-
"1"
11-
else
12-
"'t'"
13-
end
14-
when 'PostgreSQL'
15-
'TRUE'
16-
end
17-
end
18-
196
def test_no_added_joins
207
join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PostResource)
218

229
records = PostResource.records({})
2310
records = join_manager.join(records, {})
24-
assert_equal 'SELECT "posts".* FROM "posts"', records.to_sql
11+
assert_equal 'SELECT "posts".* FROM "posts"', sql_for_compare(records.to_sql)
2512

2613
assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
2714
end
@@ -31,7 +18,7 @@ def test_add_single_join
3118
join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PostResource, filters: filters)
3219
records = PostResource.records({})
3320
records = join_manager.join(records, {})
34-
assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id"', records.to_sql
21+
assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id"', sql_for_compare(records.to_sql)
3522
assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
3623
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:tags)))
3724
end
@@ -42,7 +29,7 @@ def test_add_single_sort_join
4229
records = PostResource.records({})
4330
records = join_manager.join(records, {})
4431

45-
assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id"', records.to_sql
32+
assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id"', sql_for_compare(records.to_sql)
4633
assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
4734
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:tags)))
4835
end
@@ -53,7 +40,7 @@ def test_add_single_sort_and_filter_join
5340
join_manager = JSONAPI::ActiveRelation::JoinManager.new(resource_klass: PostResource, sort_criteria: sort_criteria, filters: filters)
5441
records = PostResource.records({})
5542
records = join_manager.join(records, {})
56-
assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id"', records.to_sql
43+
assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id"', sql_for_compare(records.to_sql)
5744
assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
5845
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:tags)))
5946
end
@@ -68,7 +55,7 @@ def test_add_sibling_joins
6855
records = PostResource.records({})
6956
records = join_manager.join(records, {})
7057

71-
assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id" LEFT OUTER JOIN "people" ON "people"."id" = "posts"."author_id"', records.to_sql
58+
assert_equal 'SELECT "posts".* FROM "posts" LEFT OUTER JOIN "posts_tags" ON "posts_tags"."post_id" = "posts"."id" LEFT OUTER JOIN "tags" ON "tags"."id" = "posts_tags"."tag_id" LEFT OUTER JOIN "people" ON "people"."id" = "posts"."author_id"', sql_for_compare(records.to_sql)
7259
assert_hash_equals({alias: 'posts', join_type: :root}, join_manager.source_join_details)
7360
assert_hash_equals({alias: 'tags', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:tags)))
7461
assert_hash_equals({alias: 'people', join_type: :left}, join_manager.join_details_by_relationship(PostResource._relationship(:author)))
@@ -81,7 +68,7 @@ def test_add_joins_source_relationship
8168
records = PostResource.records({})
8269
records = join_manager.join(records, {})
8370

84-
assert_equal 'SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"', records.to_sql
71+
assert_equal 'SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"', sql_for_compare(records.to_sql)
8572
assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.source_join_details)
8673
end
8774

@@ -94,7 +81,7 @@ def test_add_joins_source_relationship_with_custom_apply
9481

9582
sql = 'SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "comments"."approved" = ' + db_true
9683

97-
assert_equal sql, records.to_sql
84+
assert_equal sql, sql_for_compare(records.to_sql)
9885

9986
assert_hash_equals({alias: 'comments', join_type: :inner}, join_manager.source_join_details)
10087
end
@@ -195,7 +182,7 @@ def test_polymorphic_join_belongs_to_just_source
195182
records = PictureResource.records({})
196183
records = join_manager.join(records, {})
197184

198-
# assert_equal 'SELECT "pictures".* FROM "pictures" LEFT OUTER JOIN "products" ON "products"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Product\' LEFT OUTER JOIN "documents" ON "documents"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Document\'', records.to_sql
185+
# assert_equal 'SELECT "pictures".* FROM "pictures" LEFT OUTER JOIN "products" ON "products"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Product\' LEFT OUTER JOIN "documents" ON "documents"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Document\'', sql_for_compare(records.to_sql)
199186
assert_hash_equals({alias: 'products', join_type: :left}, join_manager.source_join_details('products'))
200187
assert_hash_equals({alias: 'documents', join_type: :left}, join_manager.source_join_details('documents'))
201188
assert_hash_equals({alias: 'products', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'products'))
@@ -209,7 +196,7 @@ def test_polymorphic_join_belongs_to_filter
209196
records = PictureResource.records({})
210197
records = join_manager.join(records, {})
211198

212-
# assert_equal 'SELECT "pictures".* FROM "pictures" LEFT OUTER JOIN "products" ON "products"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Product\' LEFT OUTER JOIN "documents" ON "documents"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Document\'', records.to_sql
199+
# assert_equal 'SELECT "pictures".* FROM "pictures" LEFT OUTER JOIN "products" ON "products"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Product\' LEFT OUTER JOIN "documents" ON "documents"."id" = "pictures"."imageable_id" AND "pictures"."imageable_type" = \'Document\'', sql_for_compare(records.to_sql)
213200
assert_hash_equals({alias: 'pictures', join_type: :root}, join_manager.source_join_details)
214201
assert_hash_equals({alias: 'products', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'products'))
215202
assert_hash_equals({alias: 'documents', join_type: :left}, join_manager.join_details_by_polymorphic_relationship(PictureResource._relationship(:imageable), 'documents'))

0 commit comments

Comments
 (0)