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 +4 -0
- data/lib/riddle/query.rb +15 -10
- data/lib/riddle/query/insert.rb +2 -2
- data/lib/riddle/query/select.rb +21 -12
- data/riddle.gemspec +1 -1
- data/spec/functional/escaping_spec.rb +49 -0
- data/spec/riddle/query/insert_spec.rb +4 -4
- data/spec/riddle/query/select_spec.rb +31 -16
- data/spec/riddle/query_spec.rb +2 -6
- metadata +6 -4
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.
|
data/lib/riddle/query.rb
CHANGED
@@ -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
|
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 =
|
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(
|
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
|
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("
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
|
data/lib/riddle/query/insert.rb
CHANGED
@@ -14,7 +14,7 @@ class Riddle::Query::Insert
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def to_sql
|
17
|
-
"#{command} INTO #{@index} (
|
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
|
data/lib/riddle/query/select.rb
CHANGED
@@ -26,7 +26,7 @@ class Riddle::Query::Select
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def matching(match)
|
29
|
-
@matching = match
|
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(
|
107
|
+
"MATCH(#{Riddle::Query.quote @matching})"
|
108
108
|
else
|
109
|
-
"MATCH(
|
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
|
data/riddle.gemspec
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
data/spec/riddle/query_spec.rb
CHANGED
@@ -78,14 +78,10 @@ describe Riddle::Query do
|
|
78
78
|
end
|
79
79
|
|
80
80
|
describe '.escape' do
|
81
|
-
%w(( ) | - ! @ ~
|
81
|
+
%w(( ) | - ! @ ~ / ^ $ ").each do |reserved|
|
82
82
|
it "escapes #{reserved}" do
|
83
|
-
Riddle::Query.escape(reserved).should == "
|
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:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 5
|
9
|
-
-
|
10
|
-
version: 1.5.
|
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-
|
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
|