riddle 1.5.7 → 1.5.8

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY CHANGED
@@ -1,3 +1,7 @@
1
+ 1.5.8 - August 26th 2013
2
+ - Reworked escaping to be consistent and always query-safe (Demian Ferreiro).
3
+ - Escape column names in SphinxQL WHERE, INSERT, ORDER BY and GROUP BY clauses and statements (Jason Rust).
4
+
1
5
  1.5.7 - July 9th 2013
2
6
  - Respect Riddle::OutOfBoundsError instances, instead of wrapping them in ResponseError.
3
7
  - Handle boolean values for snippets options.
@@ -58,22 +58,21 @@ module Riddle::Query
58
58
  end
59
59
 
60
60
  def self.snippets(data, index, query, options = nil)
61
- data = data.gsub("'") { |x| "\\'" }
62
- query = query.gsub("'") { |x| "\\'" }
61
+ data, index, query = quote(data), quote(index), quote(query)
63
62
 
64
63
  options = ', ' + options.keys.collect { |key|
65
64
  value = translate_value options[key]
66
- value = "'#{value}'" if value.is_a?(String)
65
+ value = quote value if value.is_a?(String)
67
66
 
68
67
  "#{value} AS #{key}"
69
68
  }.join(', ') unless options.nil?
70
69
 
71
- "CALL SNIPPETS('#{data}', '#{index}', '#{query}'#{options})"
70
+ "CALL SNIPPETS(#{data}, #{index}, #{query}#{options})"
72
71
  end
73
72
 
74
73
  def self.create_function(name, type, file)
75
74
  type = type.to_s.upcase
76
- "CREATE FUNCTION #{name} RETURNS #{type} SONAME '#{file}'"
75
+ "CREATE FUNCTION #{name} RETURNS #{type} SONAME #{quote file}"
77
76
  end
78
77
 
79
78
  def self.drop_function(name)
@@ -100,11 +99,17 @@ module Riddle::Query
100
99
  end
101
100
 
102
101
  def self.escape(string)
103
- string.gsub("\\") { |match|
104
- "\\\\"
105
- }.gsub(/[\(\)\|\-!@~"\/\^\$]/) { |match|
106
- "\\\\#{match}"
107
- }
102
+ string.gsub(/[\(\)\|\-!@~\/"\/\^\$\\]/) { |match| "\\#{match}" }
103
+ end
104
+
105
+ def self.quote(string)
106
+ "'#{sql_escape string}'"
107
+ end
108
+
109
+ def self.sql_escape(string)
110
+ return Mysql2::Client.escape(string) if defined?(Mysql2)
111
+
112
+ string.gsub(/['"\\]/) { |character| "\\#{character}" }
108
113
  end
109
114
  end
110
115
 
@@ -14,7 +14,7 @@ class Riddle::Query::Insert
14
14
  end
15
15
 
16
16
  def to_sql
17
- "#{command} INTO #{@index} (#{columns_to_s}) VALUES (#{values_to_s})"
17
+ "#{command} INTO #{@index} (`#{columns_to_s}`) VALUES (#{values_to_s})"
18
18
  end
19
19
 
20
20
  private
@@ -24,7 +24,7 @@ class Riddle::Query::Insert
24
24
  end
25
25
 
26
26
  def columns_to_s
27
- columns.join(', ')
27
+ columns.join('`, `')
28
28
  end
29
29
 
30
30
  def values_to_s
@@ -26,7 +26,7 @@ class Riddle::Query::Select
26
26
  end
27
27
 
28
28
  def matching(match)
29
- @matching = match.gsub("'") { |x| "\\'" }
29
+ @matching = match
30
30
  self
31
31
  end
32
32
 
@@ -83,11 +83,11 @@ class Riddle::Query::Select
83
83
  def to_sql
84
84
  sql = "SELECT #{ @values.join(', ') } FROM #{ @indices.join(', ') }"
85
85
  sql << " WHERE #{ combined_wheres }" if wheres?
86
- sql << " GROUP BY #{@group_by}" if !@group_by.nil?
86
+ sql << " GROUP BY #{escape_column(@group_by)}" if !@group_by.nil?
87
87
  unless @order_within_group_by.nil?
88
- sql << " WITHIN GROUP ORDER BY #{@order_within_group_by}"
88
+ sql << " WITHIN GROUP ORDER BY #{escape_column(@order_within_group_by)}"
89
89
  end
90
- sql << " ORDER BY #{@order_by}" if !@order_by.nil?
90
+ sql << " ORDER BY #{escape_column(@order_by)}" if !@order_by.nil?
91
91
  sql << " #{limit_clause}" unless @limit.nil? && @offset.nil?
92
92
  sql << " #{options_clause}" unless @options.empty?
93
93
 
@@ -104,9 +104,9 @@ class Riddle::Query::Select
104
104
  if @matching.nil?
105
105
  wheres_to_s
106
106
  elsif @wheres.empty? && @where_nots.empty? && @where_alls.empty? && @where_not_alls.empty?
107
- "MATCH('#{@matching}')"
107
+ "MATCH(#{Riddle::Query.quote @matching})"
108
108
  else
109
- "MATCH('#{@matching}') AND #{wheres_to_s}"
109
+ "MATCH(#{Riddle::Query.quote @matching}) AND #{wheres_to_s}"
110
110
  end
111
111
  end
112
112
 
@@ -134,22 +134,22 @@ class Riddle::Query::Select
134
134
  def filter_comparison_and_value(attribute, value)
135
135
  case value
136
136
  when Array
137
- "#{attribute} IN (#{value.collect { |val| filter_value(val) }.join(', ')})"
137
+ "#{escape_column(attribute)} IN (#{value.collect { |val| filter_value(val) }.join(', ')})"
138
138
  when Range
139
- "#{attribute} BETWEEN #{filter_value(value.first)} AND #{filter_value(value.last)}"
139
+ "#{escape_column(attribute)} BETWEEN #{filter_value(value.first)} AND #{filter_value(value.last)}"
140
140
  else
141
- "#{attribute} = #{filter_value(value)}"
141
+ "#{escape_column(attribute)} = #{filter_value(value)}"
142
142
  end
143
143
  end
144
144
 
145
145
  def exclusive_filter_comparison_and_value(attribute, value)
146
146
  case value
147
147
  when Array
148
- "#{attribute} NOT IN (#{value.collect { |val| filter_value(val) }.join(', ')})"
148
+ "#{escape_column(attribute)} NOT IN (#{value.collect { |val| filter_value(val) }.join(', ')})"
149
149
  when Range
150
- "#{attribute} < #{filter_value(value.first)} OR #{attribute} > #{filter_value(value.last)}"
150
+ "#{escape_column(attribute)} < #{filter_value(value.first)} OR #{attribute} > #{filter_value(value.last)}"
151
151
  else
152
- "#{attribute} <> #{filter_value(value)}"
152
+ "#{escape_column(attribute)} <> #{filter_value(value)}"
153
153
  end
154
154
  end
155
155
 
@@ -188,4 +188,13 @@ class Riddle::Query::Select
188
188
  value
189
189
  end
190
190
  end
191
+
192
+ def escape_column(column)
193
+ if column.to_s =~ /\A[`@]/
194
+ column
195
+ else
196
+ column_name, *extra = column.to_s.split(' ')
197
+ extra.unshift("`#{column_name}`").compact.join(' ')
198
+ end
199
+ end
191
200
  end
@@ -3,7 +3,7 @@ $:.push File.expand_path('../lib', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = 'riddle'
6
- s.version = '1.5.7'
6
+ s.version = '1.5.8'
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ['Pat Allan']
9
9
  s.email = ['pat@freelancing-gods.com']
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'SphinxQL escaping', :live => true do
4
+ let(:connection) { Mysql2::Client.new :host => '127.0.0.1', :port => 9306 }
5
+
6
+ def sphinxql_matching(string)
7
+ select = Riddle::Query::Select.new
8
+ select.from 'people'
9
+ select.matching string
10
+ select.to_sql
11
+ end
12
+
13
+ ['@', "'", '"', '\\"', "\\'"].each do |string|
14
+ it "escapes #{string}" do
15
+ lambda {
16
+ connection.query sphinxql_matching(Riddle::Query.escape(string))
17
+ }.should_not raise_error(Mysql2::Error)
18
+ end
19
+ end
20
+
21
+ context 'on snippets' do
22
+ def snippets_for(text, words = '', options = nil)
23
+ snippets_query = Riddle::Query.snippets(text, 'people', words, options)
24
+ connection.query(snippets_query).first['snippet']
25
+ end
26
+
27
+ it 'preserves original text with special SphinxQL escape characters' do
28
+ text = 'email: john@example.com (yay!)'
29
+ snippets_for(text).should == text
30
+ end
31
+
32
+ it 'preserves original text with special MySQL escape characters' do
33
+ text = "'Dear' Susie\nAlways use {\\LaTeX}"
34
+ snippets_for(text).should == text
35
+ end
36
+
37
+ it 'escapes match delimiters with special SphinxQL escape characters' do
38
+ snippets = snippets_for('hello world', 'world',
39
+ :before_match => '()|-!', :after_match => '@~"/^$')
40
+ snippets.should == 'hello ()|-!world@~"/^$'
41
+ end
42
+
43
+ it 'escapes match delimiters with special MySQL escape characters' do
44
+ snippets = snippets_for('hello world', 'world',
45
+ :before_match => "'\"", :after_match => "\n\t\\")
46
+ snippets.should == "hello '\"world\n\t\\"
47
+ end
48
+ end
49
+ end unless RUBY_PLATFORM == 'java' || Riddle.loaded_version.to_i < 2
@@ -3,23 +3,23 @@ require 'spec_helper'
3
3
  describe Riddle::Query::Insert do
4
4
  it 'handles inserts' do
5
5
  query = Riddle::Query::Insert.new('foo_core', [:id, :deleted], [4, false])
6
- query.to_sql.should == 'INSERT INTO foo_core (id, deleted) VALUES (4, 0)'
6
+ query.to_sql.should == 'INSERT INTO foo_core (`id`, `deleted`) VALUES (4, 0)'
7
7
  end
8
8
 
9
9
  it 'handles replaces' do
10
10
  query = Riddle::Query::Insert.new('foo_core', [:id, :deleted], [4, false])
11
11
  query.replace!
12
- query.to_sql.should == 'REPLACE INTO foo_core (id, deleted) VALUES (4, 0)'
12
+ query.to_sql.should == 'REPLACE INTO foo_core (`id`, `deleted`) VALUES (4, 0)'
13
13
  end
14
14
 
15
15
  it 'encloses strings in single quotes' do
16
16
  query = Riddle::Query::Insert.new('foo_core', [:id, :name], [4, 'bar'])
17
- query.to_sql.should == "INSERT INTO foo_core (id, name) VALUES (4, 'bar')"
17
+ query.to_sql.should == "INSERT INTO foo_core (`id`, `name`) VALUES (4, 'bar')"
18
18
  end
19
19
 
20
20
  it 'handles inserts with more than one set of values' do
21
21
  query = Riddle::Query::Insert.new 'foo_core', [:id, :name], [[4, 'bar'], [5, 'baz']]
22
22
  query.to_sql.
23
- should == "INSERT INTO foo_core (id, name) VALUES (4, 'bar'), (5, 'baz')"
23
+ should == "INSERT INTO foo_core (`id`, `name`) VALUES (4, 'bar'), (5, 'baz')"
24
24
  end
25
25
  end
@@ -34,84 +34,99 @@ describe Riddle::Query::Select do
34
34
 
35
35
  it 'handles filters with integers' do
36
36
  query.from('foo_core').matching('foo').where(:bar_id => 10).to_sql.
37
- should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar_id = 10"
37
+ should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar_id` = 10"
38
38
  end
39
39
 
40
40
  it "handles exclusive filters with integers" do
41
41
  query.from('foo_core').matching('foo').where_not(:bar_id => 10).to_sql.
42
- should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar_id <> 10"
42
+ should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar_id` <> 10"
43
43
  end
44
44
 
45
45
  it "handles filters with true" do
46
46
  query.from('foo_core').matching('foo').where(:bar => true).to_sql.
47
- should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar = 1"
47
+ should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar` = 1"
48
48
  end
49
49
 
50
50
  it "handles exclusive filters with true" do
51
51
  query.from('foo_core').matching('foo').where_not(:bar => true).to_sql.
52
- should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar <> 1"
52
+ should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar` <> 1"
53
53
  end
54
54
 
55
55
  it "handles filters with false" do
56
56
  query.from('foo_core').matching('foo').where(:bar => false).to_sql.
57
- should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar = 0"
57
+ should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar` = 0"
58
58
  end
59
59
 
60
60
  it "handles exclusive filters with false" do
61
61
  query.from('foo_core').matching('foo').where_not(:bar => false).to_sql.
62
- should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar <> 0"
62
+ should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar` <> 0"
63
63
  end
64
64
 
65
65
  it "handles filters with arrays" do
66
66
  query.from('foo_core').matching('foo').where(:bars => [1, 2]).to_sql.
67
- should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bars IN (1, 2)"
67
+ should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bars` IN (1, 2)"
68
68
  end
69
69
 
70
70
  it "handles exclusive filters with arrays" do
71
71
  query.from('foo_core').matching('foo').where_not(:bars => [1, 2]).to_sql.
72
- should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bars NOT IN (1, 2)"
72
+ should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bars` NOT IN (1, 2)"
73
73
  end
74
74
 
75
75
  it "handles filters with timestamps" do
76
76
  time = Time.now
77
77
  query.from('foo_core').matching('foo').where(:created_at => time).to_sql.
78
- should == "SELECT * FROM foo_core WHERE MATCH('foo') AND created_at = #{time.to_i}"
78
+ should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `created_at` = #{time.to_i}"
79
79
  end
80
80
 
81
81
  it "handles exclusive filters with timestamps" do
82
82
  time = Time.now
83
83
  query.from('foo_core').matching('foo').where_not(:created_at => time).
84
- to_sql.should == "SELECT * FROM foo_core WHERE MATCH('foo') AND created_at <> #{time.to_i}"
84
+ to_sql.should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `created_at` <> #{time.to_i}"
85
85
  end
86
86
 
87
87
  it "handles filters with ranges" do
88
88
  query.from('foo_core').matching('foo').where(:bar => 1..5).to_sql.
89
- should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar BETWEEN 1 AND 5"
89
+ should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar` BETWEEN 1 AND 5"
90
90
  end
91
91
 
92
92
  it "handles filters expecting matches on all values" do
93
93
  query.from('foo_core').where_all(:bars => [1, 2]).to_sql.
94
- should == "SELECT * FROM foo_core WHERE bars = 1 AND bars = 2"
94
+ should == "SELECT * FROM foo_core WHERE `bars` = 1 AND `bars` = 2"
95
95
  end
96
96
 
97
97
  it "handles exclusive filters expecting matches on none of the values" do
98
98
  query.from('foo_core').where_not_all(:bars => [1, 2]).to_sql.
99
- should == "SELECT * FROM foo_core WHERE (bars <> 1 OR bars <> 2)"
99
+ should == "SELECT * FROM foo_core WHERE (`bars` <> 1 OR `bars` <> 2)"
100
100
  end
101
101
 
102
102
  it 'handles grouping' do
103
103
  query.from('foo_core').group_by('bar_id').to_sql.
104
- should == "SELECT * FROM foo_core GROUP BY bar_id"
104
+ should == "SELECT * FROM foo_core GROUP BY `bar_id`"
105
105
  end
106
106
 
107
107
  it 'handles ordering' do
108
108
  query.from('foo_core').order_by('bar_id ASC').to_sql.
109
- should == 'SELECT * FROM foo_core ORDER BY bar_id ASC'
109
+ should == 'SELECT * FROM foo_core ORDER BY `bar_id` ASC'
110
+ end
111
+
112
+ it 'handles ordering when an already escaped column is passed in' do
113
+ query.from('foo_core').order_by('`bar_id` ASC').to_sql.
114
+ should == 'SELECT * FROM foo_core ORDER BY `bar_id` ASC'
115
+ end
116
+
117
+ it 'handles ordering when just a symbol is passed in' do
118
+ query.from('foo_core').order_by(:bar_id).to_sql.
119
+ should == 'SELECT * FROM foo_core ORDER BY `bar_id`'
120
+ end
121
+
122
+ it 'handles ordering when a computed sphinx variable is passed in' do
123
+ query.from('foo_core').order_by('@weight DESC').to_sql.
124
+ should == 'SELECT * FROM foo_core ORDER BY @weight DESC'
110
125
  end
111
126
 
112
127
  it 'handles group ordering' do
113
128
  query.from('foo_core').order_within_group_by('bar_id ASC').to_sql.
114
- should == 'SELECT * FROM foo_core WITHIN GROUP ORDER BY bar_id ASC'
129
+ should == 'SELECT * FROM foo_core WITHIN GROUP ORDER BY `bar_id` ASC'
115
130
  end
116
131
 
117
132
  it 'handles a limit' do
@@ -78,14 +78,10 @@ describe Riddle::Query do
78
78
  end
79
79
 
80
80
  describe '.escape' do
81
- %w(( ) | - ! @ ~ " / ^ $).each do |reserved|
81
+ %w(( ) | - ! @ ~ / ^ $ ").each do |reserved|
82
82
  it "escapes #{reserved}" do
83
- Riddle::Query.escape(reserved).should == "\\\\#{reserved}"
83
+ Riddle::Query.escape(reserved).should == "\\#{reserved}"
84
84
  end
85
85
  end
86
-
87
- it "escapes \\" do
88
- Riddle::Query.escape("\\").should == "\\\\"
89
- end
90
86
  end
91
87
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: riddle
3
3
  version: !ruby/object:Gem::Version
4
- hash: 13
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
8
  - 5
9
- - 7
10
- version: 1.5.7
9
+ - 8
10
+ version: 1.5.8
11
11
  platform: ruby
12
12
  authors:
13
13
  - Pat Allan
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2013-07-09 00:00:00 +10:00
18
+ date: 2013-08-26 00:00:00 +10:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -244,6 +244,7 @@ files:
244
244
  - spec/fixtures/sql/data.tsv
245
245
  - spec/fixtures/sql/structure.sql
246
246
  - spec/functional/connection_spec.rb
247
+ - spec/functional/escaping_spec.rb
247
248
  - spec/functional/excerpt_spec.rb
248
249
  - spec/functional/keywords_spec.rb
249
250
  - spec/functional/persistance_spec.rb
@@ -437,6 +438,7 @@ test_files:
437
438
  - spec/fixtures/sql/data.tsv
438
439
  - spec/fixtures/sql/structure.sql
439
440
  - spec/functional/connection_spec.rb
441
+ - spec/functional/escaping_spec.rb
440
442
  - spec/functional/excerpt_spec.rb
441
443
  - spec/functional/keywords_spec.rb
442
444
  - spec/functional/persistance_spec.rb