riddle 1.5.7 → 1.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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